Unit testing is an established and integral part of the software development process, especially when using .NET. If you are not aware, the basic assumption is to test the public methods of the classes so that they work as expected.
Unit tests shall comply with 3A: Arrange, Act, and Confirm. In other words, specify the test conditions, run the code, and finally verify that it works as expected. If successful, then the test went just fine! If not, then there is a task to be done.
This article looks at the first step, organizing the test. Although there are a few tasks at this point, we are currently only interested in creating the information needed to complete the test.
Later, we will look at how to create complex test data that can be easily understood using the Builder model.
Why is it important how test data is created?
Test data can be confusing when viewed and understood. Monitoring and updating is time consuming, especially when the test data has relationships to maintain. If the test data is not clear, it is easy to make a mistake and add errors to the test code.
Test data written long ago is hard to remember. As with everything, it’s fresh in mind when writing, but three or six months later, it’s hard to remember the purpose of the information. It is easy and very tempting to break at times DRY (Do not repeat yourself) principle and repeat only the test data. When new tests are prepared, test data that is not well understood is copied and modified as intended. This only increases the technical debt in the test data, making it difficult to maintain the entire project.
Organizing test data
Most, if not all, codes depend on the parameters or data to be executed. So when it comes to unit testing and code functionality, some data is usually needed to perform the test. This can range from a simple value type parameter to a complex object model or even broader related information. There are different ways to create unit test data, and we’ll look at some of them briefly.
Test data can be created in a unit test directly, passed as a test in a row, downloaded from files, or created for use in a memory database. I think the memory database is great for unit testing. A separate database maintained outside the test execution process would be considered as integration testing.
Adding test data using the xUnit test frame
I use it to manage and perform unit tests xUnit. It’s a popular, free, open source, and community-centric unit testing framework that provides a few useful ways to manage our test data.
Theory unit test
Theory unit tests allow a single test to operate on sets of parameterized data. The data is transmitted using the InlineData data attribute in as many different cases as needed. This keeps the test code dry and eliminates the need to repeat the test logic for different data values.
In cases where the test data is complex, xUnit also supports the ClassData and MemberData attributes to load test data from a method or another class. xUnit is extensible and custom data attributes can be written, for example, to download test data from files or a database.
The last xUnit feature to look at is the Fixture class. The fixtures allow the installation and cleaning code to be shared for all tests in a class or even multiple test categories. This is a great place to create complex test data and the perfect place to create a memory database for tests that require it.
In our development, we often use SQL Server, which can be accessed through the Entity Framework, and to test the SQLite memory database to simulate the database layer. We use this approach to initialize a database in memory with the information we need, which provides a series of logically grouped test sets for the context set.
One neat library for creating and creating test data is AutoFixture. The authors describe it as follows:
AutoFixture is an .NET open source library designed to minimize the process of organizing unit tests to maximize maintenance. Its primary goal is to allow developers to focus on the position under test instead of defining a test scenario by facilitating the creation of object diagrams containing test data.
We don’t use AutoFixture to create test data in this example, but it’s worth mentioning because it can speed up test writing.
How Builder Pattern can help create complex data
As mentioned earlier, there are problems with the complexity of test data. Fortunately, there is an approach we can use to help that has been around for a long time. The builder pattern is a well-understood design model in software development. It allows you to build complex objects step by step and can be used to create different representations of complex data.
This fits well with our needs because we can use this approach to create our test data. The builder pattern allows us to create our test data on purpose. It means creating a concrete implementation of the model with knowledge of the area.
However, I think this approach can be useful because it gives us the following benefits:
- One place to display objects.
- It prevents data from being repeated.
- Provides a way to manage every step of the construction process. This results in the code providing a clear purpose for the data.
- A concrete implementation can be developed in a domain-specific language. This also clarifies the purpose of the data. Name the builder methods so that you want to give the most information to the developer / tester.
- Because the structure of the object is encased in code, it can be designed to handle foreign keys in code, which reduces complexity.
- It also means that collections are created as needed. Collections can be navigated using the last item by agreement.
Builder pattern in action
In this project I used .NET Core 5.0, EFCore and SQLite. Also included is xUnit for unit testing, AgileMapper for mapping, and Fluent Assertions for claims.
Our the demo project is an evolving route planner and is available on GitHub. It is model-based around tourists with excursions that include visits to places (e.g. Paris) and visits to places of interest (e.g. Eiffel Tower).
The only business logic is to change the order of the destinations during the trip. This logic is code to be tested, has a database dependency, and is encapsulated in a
So now for the information. As mentioned earlier, we simulate the database using the in-memory SQLite database, as this works well with our service under test. This is added using the xUnit attachment class so that all tests in the class can share the same information. Finally, we have a custom implementation tool for creating test data.
What does Test Builder look like?
ITourist the interface includes ways to create our tourists. You can see that the build tool allows you to build a complex object model step by step using method names that are domain-relevant. Finally,
BuildTourist method to restore our object.
Now a sample of the implementation. First, we create our parent object, a
Tourist held in the private sector.
Next we add
Excursion for our tourists.
All other steps are similar to the above to build the object. Finally,
BuildTourist a method that returns us
The test project includes an example of downloading data using the Builder template. The order and indentation of items are important for readability, but are not essential.
In contrast, it also includes an example of loading the same test data using direct object creation.
When comparing these two different implementations, it’s easy to see that with Builder, test data is more readable and easier to maintain.
Now go to the unit tests. The
TestsWithFixtureAndBuilder the class uses Fixture, where the test data is created. The test class includes one test to verify that:
SwapPlaceVisits the method works as expected.
Be sure to check the whole code example on GitHub.
More ideas for the test data builder
Because this is custom code, there is a huge amount of flexibility available, and you can improve the test data builder by following these steps:
- Include validation rules. This can be useful in verifying the correctness of business logic constraints before performing a test and in ensuring that test data is generated correctly.
- Set the master keys automatically. Depending on how the tests are configured in your case, it may be possible to set the primary keys in the build tool.
- Create a template with defaults and update only certain values. This can be done using lambda functions.
- Add simple English descriptions to the builder and skip the ToString () method. Text information can be added to a line. This can also be printed during unit testing to facilitate debugging of problematic tests.
- Add factory methods to generate general test data.
As we’ve seen, there are many ways to create test data. However, as data begins to become complex, it can be difficult to understand and maintain, leading to complications in our unit tests. The builder pattern has been around for a long time and has proven useful in creating test data to provide context and facilitate the understanding and maintenance of test data.
Hopefully this article and code example will provide some helpful tips for building your test data. In addition to this specific code example, the takeaways are to find the approach that works best for you and your other development and testing team. So when creating test data, try the following:
- The test data is easy to understand.
- The relationship between the data is easy to understand.
- The information is easy to change or add.
- The approach ensures that developers adhere to the DRY principle.