Keeping tests valuable: Unit Tests in Software Domain
The problem is not that testing is the bottleneck. The problem is that you don’t know what’s in the bottle. That’s a problem that testing addresses!
The phrase that appears in the subtitle of this post was said by Michael Bolton:
The problem is not that the test is the bottleneck. The problem is that you don't know what is in the bottle. That is a problem that tests address!
It is funny that the more we work with software tests we realize that we still need to improve, it is clear that the road to achieving an acceptable standard of quality when developing software is still a long way off. Perfection in testing will never exist! Always some features within the software will not contain all the necessary tests. We will always fail at some point, whether when mocking or working with large pieces of software of high complexity. But the focus of this post is to give tips on what can help us start testing the right way. So the topic is unit testing in the software domain. You can never miss these tests! Let's understand better how essential these tests are.
Understand the heart of software
We know that many times the domain of all software is treated as if it were the heart of the application because it is where the real value of the software is. It represents the "raison d'être" of the software. The core of the software is the reason why a customer would buy the software over any other. So if a customer hires a programmer to solve a problem, the initial step to be successful in developing this solution is to understand and be aligned with the domain expert (customer/party interested in the solution). So why wouldn't this essential part have tests that check units of the code? But at the same time, what is the point of writing tests for a domain that you don't understand clearly, that always generates doubts? Trying to write these tests without first clearly understanding the problem and having proposed the solution together with the client will simply generate work that will only be a waste of time. If the data modeling and the relationships between tables were thought out, planned and developed in the wrong way, the same will apply to the tests! So the first observation is always to avoid writing test cases without first having a solid understanding of the domain you are working in.
But for many developers, the reality is different. There is no adequate time to understand the full domain of the software. Sometimes this software does not even have a clear domain for the client itself. In these cases, try to dive into the software domain on your own, write down questions, and raise them with the team. Look for the right people within the project who can direct you to the right way about how the application rules work. This will make your job much easier and improve code writing and testing. So if you find some tests that don't make sense or are even useless because they are not checking for any behavior, try to understand with the development team if that test needs to exist.
For example, imagine an application that has a voucher class in its domain that grants discounts on products. So one voucher can have just one percentage of discount over N products that can have several categories. This can differ a lot from one business rule to another. Going further we can apply more than one discount voucher (with different codes) over the same product? If so, how do we cover the behavior of this scenario? Furthermore, the customer wants to be able to check whether that voucher has already been used or not by the logged-in user. But what if when writing the code the developer does not know the domain in a solid way some mistake can be made? Yes, for example, it can happen to include in the logic that if the user has already used that voucher, the discount is marked as inactive and other users are not able to use that same voucher. But the functionality required that if a user used the voucher, that same voucher could not be used again in the purchase of any product only for the user who made the purchase.
Writing the code in the wrong way, the tests follow that same path. But what was the problem? The developer is not aligned with the application domain. The developer needs to study the heart of the software he is working for to get the highest possible performance and increase productivity and more importantly the quality of the solution for the end user. Thinking about these points will help you both in the code you are writing and also in the test writing, so with each new feature, I encourage you to go beyond and question, investigate the domain, and understand what is going on in the core of the software. Now let's talk about the importance of the critical functionality that all software contains.
Identify critical functionality
We can use the same example as in the previous topic, the voucher class contains critical functionality because it deals with discounts on products. The vouchers have a discount percentage that will be applied over the real value of the product and this real value tends to decrease whenever a percentage of 5, 10 percent or more is applied. So if the developer does not understand the full functionality of the voucher itself, what is the chance of a bug being introduced? It is very high! And even more, if he writes the tests with the wrong understanding of the domain. And this can greatly affect the way the application works, hurting the end user and the very customer who bought the solution (the software). Inadequate testing of critical functionality can financially affect companies and users, so our attention and care are essential throughout the process. The critical and detailed look can save weekends and contracts!
Let's change the scenario a bit and think about this critical voucher function in development. If this class contains a property called the quantity, which seeks to provide visibility into the quantity of that voucher type available for certain promotions, and has a validation that allows a maximum voucher quantity, say 30 quantities of that voucher type with a unique ID should be available until the expiration date is passed. How should this be tested? The test should provide confidence, ensuring that if a quantity greater than 30 is applied for the voucher, this should not be valid in the system, preventing anyone from registering this information. But does such a test exist? Is this behavior being covered to ensure that the developer did not make a mistake when writing the validation?
That's why it is essential after diving into the software domain to understand its business-critical points! Critical points are rarely analyzed carefully, so many times financial institutions are scammed or have been scammed because of failed validation rules in their business. Often these lack of testing are called loopholes and lack behavioral testing seeking to carefully analyze critical functionality. The most recent company that I remember that suffered from such a breach was C6 Bank and Github. Anyway, the point I want to make clear is that domain testing goes far beyond just writing tests. It is understanding the solution and its main critical points!
Worry about inputs and outputs
When we talk about testing the inputs and outputs, we are not just talking about the happy path. But analyze all possible scenarios! This goes far beyond testing for example a scenario similar to this one:
- the result of calling function X with input A should be output B.
This test must be done for your domain. But the incentive of this topic is to look for outputs that we don't expect, for example, to think about this situation:
can the result of calling function X with input C generate something other than B?
How does the software domain respond to unexpected input? Is there a treatment for invalid inputs?
This questioning makes the developer go beyond the basic test scenario. We need to find possible failures in the inputs and outputs. Some rules that the end user can break by passing different values as input.
Again let's turn to the Voucher domain class. If a user enters an invalid discount voucher the expected return is that this discount voucher can never be valid. But what if the user thinks of another alternative? Perhaps send a request that is more faithful to what the application expects. Even voucher codes that may be registered in your database? The developer might think, "If this voucher code is in my base, it is legitimate!" But we can't always be 100% sure! Hasn't the service that generated the discounts or even a job in the business area responsible for making these entries made some mistake?
Another example to consider, is when a promotion occurs and vouchers are generated, does this apply to all products in a certain category? Test cases to cover this behavior and ensure that the functionality is in accordance helps a lot to avoid headaches in the future. Does my domain care about a voucher expiration date? If yes, what happens to a voucher when the expiry date is less than the current date? Normally the discount should be unavailable because that voucher has expired. But does this behavior occur? The only way to guarantee this is to create tests and plan input and output scenarios.
Another factor to note is that this input and output data should always be data that tries its best to simulate reality. So a single domain class can have numerous tests to validate essential behaviors. In the voucher case testing the inputs may seem simpler, but we need to make sure we have a solid understanding of the domain. Code implementation in Domain 👇🏼:
protected static bool IsValidDateExpiration(DateTime expirationDate)
{
return expirationDate >= DateTime.Now;
}
[Fact(DisplayName = "Validate Voucher Invalid Expiration Date")]
[Trait("Category", "Sales - Voucher")]
public void Voucher_ValidateVoucher_MustBeInvalid()
{
var voucher = new Voucher("PROMO-15-OFF", 15, null, 1,
TypeDiscountVoucher.Percentage, DateTime.Now.AddDays(-1), false, true);
// act
var result = voucher.ValidateIfApplicable();
// Assert
Assert.False(result.IsValid);
Assert.Equal(1, result.Errors.Count);
Assert.Contains(VoucherApplicableValidation.ValidationErrorMsg, result.Errors.Select(c => c.ErrorMessage));
}
Above is an example of a test that has the objective of verifying that when the behavior of a voucher date is less than the current date, we have the assert to guarantee that the behavior will be treated as invalid. If the expiration date is higher than the current date, we know that the voucher is valid, but if the expiration date is lower than the current date, we are sure that this voucher is expired.
Keep the ubiquitous language in your tests
Over these years, I have worked on many different projects, with totally different purposes and rules. One thing I have noticed. Many projects that followed a rich software modeling, got lost in their integration and unit tests, the ubiquitous language was clear to the team, but when writing the tests we could see that many did not maintain the quality in writing the scenarios and also the test case itself. But why? I have a strong argument for this. It happens because we always leave the tests last! What happens when the sprint or the deadlines get short? The tests end up being done carelessly! Without quality! Unfortunately, I have already worked and had to upload classes or methods with tests only with the happy path that the code followed. This is something I am trying to avoid as much as possible these days. But the point I want to make is that we need to say that a task is complete only when all the test scenarios are completed. Also, we need to bring the ubiquitous language into testing as well! Because if this doesn't happen, I am sure that few people will remember after a month what exactly that testing seeks to verify in the software.
We must not forget that ubiquitous language can change during the project. So don't neglect your tests when these changes occur.
To make it clear, consider the following scenario. In commercial software, we have an Enum class that contains all the statuses of an issued invoice. Let's assume that for invoices that have been deleted, we have the status inactive. For the client interested in the solution, the famous domain experts, there is no possibility to delete an invoice. It just stays with the inactive status. But this changes after a few months. Now the system needs to display the status as deleted. So the changes happen in the code base, but old developers don't update the test description or the test method name. So if someone with little technical knowledge tries to generate documentation based on the tests, we won't have reliable documentation. Another scenario to look at is when any functional business term is mentioned in alignment meetings, terms common to domain experts, but foreign to developers and QA's. If this happens in your project, your testing will probably be affected as well.
For us developers everything seems simple from one perspective because everyone who uses an app, ERP platform or software, CMS and so on is a user. But for domain experts, Product Owners, Product Managers and other team members the perspective is different. They all try to be 100% aligned with the business experts. So maybe the experts do not use the word user, maybe for the business it is a company, a bank or even a specific group of users like patients, doctors, or affiliates. I am not saying that they are wrong. On the contrary, they are right to keep this perspective, we developers are starting to realize that the more we are aligned with the product experts, the more the code base becomes reliable to meet the needs and solve the problem of the end customer. And even more so the testing will be.
Conclusion
The four topics we have seen serve to alert us to how the software domain is being tested. Quality is essential! Yes, it is easier to write than to practice daily. So like everyone else who is reading this post, I strive to apply these practices when writing the tests in the projects I am working on, but I confess that I also need to improve! Avoid hassles and try to look beyond the happy path in your domain.
There are other topics we can talk about unit testing in the core of the software, for example:
Collaborate with domain experts.
Collaborate with QA's.
Test all the boundaries of your domain.
How to make it easy for non-technical people to read testing.
The importance of avoiding flaky tests.
Coverage metrics and branch coverage metrics.
These are essential topics, which will be left for a future article.
Thanks for reading and please write any constructive criticism in the comments. As Officer Spock said: "Live long and prosper".
References:
Effective Software Testing: A Developer's Guide - Mauricio Aniche
Unit Testing Principles, Practices, and Patterns - Vladimir Khorikov
Domain-Driven Design: Tackling Complexity in the Heart of Software - Eric Evans