How we made our Unit Tests Domain Oriented

Oktay Dağdelen
Trendyol Tech

--

The goal of unit testing is to verify that every unit of the software performs as designed. Writing unit tests helps you anticipate future errors and understand what you’re testing when developing software for applications with extensive workflows and user interaction.

The first decacorn of Turkey, Trendyol, has approximately 30 million customers. Thanks to this, we attach great importance to writing unit tests.

As the Trendyol Storefront Marketing Team, we have set a unit test code coverage target of at least 80% for code blocks containing business requirements on our pipeline. This requirement applies when merging from branches to the master and deploying them to the production environment.

In this code structure where unit testing is vital, we have realized that even the slightest code change requires us to modify most of our unit tests due to the ever-increasing business requirements.

In a microservice where business needs are constantly growing and changing, we had 28 unit tests. As shown in the image below, a 3-line code change resulted in exactly 9 of them failing.

Here was the results;

When we encountered these issues and noticed that the project’s structure was becoming highly coupled, we took action to make our unit tests loosely coupled. We achieved this by following these 3 steps as needed;

Contents

  • Domain Driven Design ( Identifying bounded contexts well )
  • Specification Pattern ( Reusable domain rules )
  • Optional Parameters on Domain Services ( Independent functions )

Effect of Domain Driven Design on Unit Test

Domain-Driven Design (DDD) can greatly enhance the expressiveness and modularity of your code, making it a perfect fit for unit testing.

One of the primary benefits of adopting a domain-driven approach when starting a new project is the emphasis on understanding the true needs of the business domain.

As the Storefront-Marketing Team at Trendyol, we manage both the International and Turkey sites of banners. Our responsibilities include creating, modifying, and displaying banners at specific time intervals on various page types, such as search, product detail, and side menu, across web and mobile-web platforms. Trendyol, with 22.3 million page views in June 2023 for storefronts, necessitates our ability to handle an equivalent number of requests.

For the Banner Service, the requirements of the international and Turkey sides were entirely different, and the crucial step we took was to define well-structured bounded contexts.

If we code against this domain requirement explicitly, then we can focus the test not on fetching from the database, message broker, http connection etc. but on the interaction of the domain concepts. Thanks to behavior like that, our tests looked like;

There’s no need to mock around with the banner. Our main focus was to ensure that it exhibits the expected behavior of the aggregate root.

How the Specification Pattern made our unit testing easier

The specification object has a clear and limited responsibility, which can be separated and decoupled from the domain object that uses it.

defined in paper whose name is Specifications by Eric Evans & Martin Fowler.

By defining business rules as specifications, we gain the flexibility to use them across various roles within the domain. This approach not only allows us to test different scenarios effectively but also enables us to adopt a table-driven structure for test writing, as illustrated in the example above.

For instance, a single specification can be utilized for different domain functions, such as creating and editing claims, even if they have different numbers of rules. This reusability eliminates the need for redundant rewriting and streamlines the testing process.

Independence for unit tests with Optional Parameters

In Banner Service, due to new business requirements, we had to add new parameter on Domain Service such as createClaimSpecification, cultureTreeAsSlice etc...

 s := international.NewDomainService(
repo,
conf,
logger) // as-is

s := international.NewDomainService(
repo,
conf,
logger,
createClaimSpecification) // with-new-requirement

When we added new params to use only in Claim Create function, all unit tests on domain service failed on build-time because of not enough arguments to call it as below

Even if it wasn’t necessary, we had to feed this mocked parameter in Domain Service such as Delete, Update, Complete Claim functions.

Unlike software languages such as Java and C#, Golang does not support optional function parameters. Besides this, one and most graceful way of many ones is using functional option.

The functional option is a pattern in that you can declare an opaque Option type that records information in some internal struct.

s := international.NewDomainService(
repo,
conf,
logger,
international.WithCreateClaimSpecification(&createClaimSpecification)) // with-new-requirement

Thanks to this, we could give extra parameters on domain service for only usage of ClaimCreate(), nothing has changed in the code of other unit tests.

Conclusion

Trendyol is a platform that contains many different domains, and can show continuous development and encounter new demands in the most appropriate and fastest way. This may require continuous change in software processes.

The importance of sustainability in the code and unit tests is increasing. There are of course many methods to build a Loose Coupled structure, and as the Storefront marketing team, we want to explain the optimizations we made while writing unit-test.

In summary, embracing Domain-Driven Design principles, utilizing the Specification Pattern for reusable domain rules, and leveraging optional parameters on Domain Services have enabled us to enhance the expressiveness, modularity, and reusability of our unit tests. By ensuring a well-defined bounded context and fostering loose coupling, we can adapt to evolving requirements efficiently without disrupting other parts of the codebase.

The optimization measures we implemented in our unit tests, have significantly improved our deployment frequency. By reducing change fail occurrences and minimizing lead time, we can swiftly respond to new demands in Trendyol’s ever-evolving platform.

If you want to be part of a team that tries new technologies and want to experience a new challenge every day, come to us.

--

--