Testing Principles
Last modified on Tue 28 Nov 2023
Tests in projects help us create stable applications and we should do them as a part of any project. While there are different kinds of tests, we mostly write unit and integration tests in .NET development.
Even though the tests are a part of the source code, their purpose and usage are different from the rest of the source code which implements the business logic. This means that the principles for writing tests are a bit different. Tests should be easy to understand. Here are some of the most important principles we follow:
- Follow the same project structure - Tests are always written in a separate project from the one we're testing, as we already mentioned in the Project Structure section of this handbook. Each source project should have its own test project, and each test project should follow the same structure as the project it is testing.
- Arrange, Act, Assert - Follow the AAA pattern
- Don't follow the DRY principle - The test must be self-contained, which means that we allow for more repetitions than we would in the rest of the source code. This doesn't mean that we should write everything from scratch for each test, but rather repeat everything that is needed to understand the test.
- Avoid multiple inheritances - While it is perfectly fine to implement a base test class, we should avoid inheriting from test classes that already inherit from some other base test classes.
- Define setups for each test method separately - Each test method should clearly define its own setups and mocks. If multiple test methods require the same setups, those can be abstracted to a separate method, just be sure to call those methods from test methods rather than from constructors.
- Minimize the number of assertions - Each test should focus on one aspect of the code. For example, if a method must validate some parameters, call a database and return an object, don't assert all three aspects in a single test method even if their setup is the same. By separating the assertions into separate tests we can easily detect what went wrong if a test fails.
- [PropertyOrMethodName]_[Case]_[ExpectedResult] - Each test method should follow the same naming convention: write the name of the method or property that is being tested, then specify the test case, and lastly specify the expected result (e.g.
UpdateUser_WhenUserDoesntExist_ThenThrowNotFoundException
).