Recent Posts
Archives

Posts Tagged ‘TestDrivenDevelopment’

PostHeaderIcon [NDCMelbourne2025] TDD & DDD from the Ground Up – Chris Simon

Chris Simon, a seasoned developer and co-organizer of Domain-Driven Design Australia, presents a compelling live-coding session at NDC Melbourne 2025, demonstrating how Test-Driven Development (TDD) and Domain-Driven Design (DDD) can create maintainable, scalable software. Through a university enrollment system example, Chris illustrates how TDD’s iterative red-green-refactor cycle and DDD’s focus on ubiquitous language and domain modeling can evolve a simple CRUD application into a robust solution. His approach highlights the power of combining these methodologies to adapt to changing requirements without compromising code quality.

Starting with TDD: The Red-Green-Refactor Cycle

Chris kicks off by introducing TDD’s core phases: writing a failing test (red), making it pass with minimal code (green), and refactoring to improve structure. Using a .NET-based university enrollment system, he begins with a basic test to register a student, ensuring a created status response. Each step is deliberately small, balancing test and implementation to minimize risk. This disciplined approach, Chris explains, builds a safety net of tests, allowing confident code evolution as complexity increases.

Incorporating DDD: Ubiquitous Language and Domain Logic

As the system grows, Chris introduces DDD principles, particularly the concept of ubiquitous language. He renames methods to reflect business intent, such as “register” instead of “create” for students, and uses a static factory method to encapsulate logic. His IDE extension, Contextive, further supports this by providing domain term definitions across languages, ensuring consistency. By moving validation logic, like checking room availability, into domain models, Chris ensures business rules are encapsulated, reducing controller complexity and enhancing maintainability.

Handling Complexity: Refactoring for Scalability

As requirements evolve, such as preventing course over-enrollment, Chris encounters a race condition in the initial implementation. He demonstrates how TDD’s tests catch this issue, allowing safe refactoring. Through event storming, he rethinks the domain model, delaying room allocation until course popularity is known. This shift, informed by domain expert collaboration, optimizes resource utilization and eliminates unnecessary constraints, showcasing DDD’s ability to align code with business needs.

Balancing Testing Strategies

Chris explores the trade-offs between API-level and unit-level testing. While API tests protect the public contract, unit tests for complex scheduling algorithms allow faster, more efficient test setup. By testing a scheduler that matches courses to rooms based on enrollment counts, he ensures robust logic without overcomplicating API tests. This strategic balance, he argues, maintains refactorability while addressing intricate business rules, a key takeaway for developers navigating complex domains.

Adapting to Change with Confidence

The session culminates in a significant refactor, removing the over-enrollment check after realizing it’s applied at the wrong stage. Chris’s tests provide the confidence to make this change, ensuring no unintended regressions. By making domain model setters private, he confirms the system adheres to DDD principles, encapsulating business logic effectively. This adaptability, driven by TDD and DDD, underscores the value of iterative development and domain collaboration in building resilient software.

Links:

PostHeaderIcon [DevoxxPL2019] Crafting Effective Automated Tests: Insights Beyond Conventional Wisdom

Lecturer

Jacek Milewski serves as a senior software developer at Circle K, where he focuses on backend Java development in domains like fuel retail and electric vehicles. As a trainer at Bottega IT Minds, he conducts sessions on domain-driven design and software architecture, drawing from his experience as a consultant, speaker, and mentor in the IT community.

Abstract

This analysis investigates approaches to automated unit and integration testing in modular applications, emphasizing practical techniques for ensuring business logic integrity. It explores test builders for entity construction, in-memory versus real repositories, and the role of test-driven development in maintaining quality. Through a live-coded example of rating calculations based on age and name length, it evaluates methodologies for edge case coverage, assertion strategies, and the balance between speed and thoroughness, while considering implications for development velocity and software reliability.

Establishing Test Foundations: From Basic Assertions to Modular Design

Automated testing forms the bedrock of reliable software, yet many practitioners grapple with adapting strategies to evolving ecosystems. Jacek commences by underscoring the perpetual relevance of testing, as technological advancements continually introduce new challenges. His methodology revolves around a simple yet comprehensive example: computing a person’s rating from age and name length, where ratings range from 0 to 100, with penalties for ages under 18 or over 65, and bonuses for longer names.

Initial tests focus on isolated units, such as a rating calculator class. Here, inputs are mocked or directly provided, verifying outputs against expectations. For instance, a test might instantiate a person with age 20 and name “John Doe,” asserting the rating equals age plus name length, capped at 100. This isolates logic, ensuring purity without external dependencies.

As complexity grows, modularization becomes key. Jacek advocates separating concerns: entities hold data, services compute logic, repositories persist state. Tests then target these layers individually, using builders to construct test data fluently. A PersonBuilder might chain methods like withAge(25).withName(“Alice”).build(), promoting readability and reuse.

Contextually, this stems from real-world projects at Circle K, where business rules like vehicle charging require verifiable implementations. Analytically, such isolation accelerates feedback loops, catching defects early. However, over-isolation risks missing integration issues, necessitating complementary tests.

Implications extend to team dynamics: standardized builders reduce onboarding time, fostering consistency. Yet, excessive abstraction can obscure intent, demanding balance.

Integrating Dependencies: Balancing Unit and Integration Testing

Transitioning to dependencies, Jacek differentiates unit tests—focusing on isolated behavior—from integration tests, verifying interactions. For persistence, in-memory repositories simulate databases, allowing rapid execution without external setups.

In the rating scenario, a service saves rated persons to a repository. Unit tests inject mock repositories, asserting save invocations and contents. Code might resemble:

PersonBuilder builder = new PersonBuilder();
Person person = builder.withAge(30).withName("Bob").build();
RatingService service = new RatingService(new InMemoryRepository());
service.calculateAndSave(person);
assertEquals(1, repository.size());
assertEquals(34, repository.get(0).getRating());

This confirms logic without I/O overhead.

For integration, swap to real repositories (e.g., JPA with H2), reusing test structures. Jacek copies unit tests, altering only the injected repository, ensuring end-to-end validation with minimal duplication.

Methodologically, this dual approach leverages TDD: write failing tests, implement minimally to pass, refactor safely. Failing tests validate coverage—green from inception might overlook assertions.

Analytically, in-memory speeds iterations, while real databases catch schema mismatches. Implications: enhanced confidence in deployments, though integration suites slow CI pipelines, suggesting selective execution.

Optimizing for Development Speed: Dispelling Myths on Testing Overhead

A prevalent myth posits testing impedes velocity, yet Jacek counters with empirical observations: initial setups invest time, but yield dividends in maintainability. Without tests, early features deploy swiftly, but regressions mount, stalling progress.

Contrastingly, test-first approaches start slower—configuring builders, mocks—but sustain pace, as refactors preserve functionality. In his experience, untested codebases accrue debt, while tested ones enable fearless enhancements.

Methodologically, focus on meaningful assertions: verify behaviors, not implementations. For empty repositories, assert isEmpty() post-setup, confirming state.

Analytically, coverage metrics mislead if superficial; aim for edge cases like invalid ages or names. Implications: teams adopting this outpace untested counterparts long-term, delivering quality sustainably.

Broader Ramifications: Testing as a Catalyst for Quality Delivery

Testing transcends verification, shaping designs toward modularity. Jacek’s Circle K tenure illustrates: robust tests facilitate microservices evolution, aligning with business agility in retail.

Yet, no universal formula exists; adapt to domains—unit for logic, integration for persistence. Implications: cultivates culture valuing prevention over remediation, elevating software craftsmanship.

In summation, these practices, honed through experience, empower developers to deliver verifiable value efficiently.

Links: