Unit Tests
For Unit Tests, you don't need to much infrastructure. You typically instantiate your class and provide some pre-configured mocked objects to prepare your object to test.
Classes Without Dependencies
In this simplest case, the class you want to test has no dependencies. In this case, you can directly instantiate your class, call its methods and make your assertions.
Example: Testing an Entity
Assume that you've an Issue
entity as shown below:
Notice that the IsClosed
and CloseDate
properties have private setters to force some business rules by using the Open()
and Close()
methods:
- Whenever you close an issue, the
CloseDate
should be set to the current time. - An issue can not be re-opened if it is locked. And if it is re-opened, the
CloseDate
should be set tonull
.
Since the Issue
entity is a part of the Domain Layer, we should test it in the Domain.Tests
project. Create an Issue_Tests
class inside the Domain.Tests
project:
This test follows the AAA (Arrange-Act-Assert) pattern;
- Arrange part creates an
Issue
entity and ensures theCloseDate
isnull
at the beginning. - Act part executes the method we want to test for this case.
- Assert part checks if the
Issue
properties are same as we expect to be.
[Fact]
attribute is defined by the xUnit library and marks a method as a test method. Should...
extension methods are provided by the Shouldly library. You can directly use the Assert
class of the xUnit, but Shouldly makes it much comfortable and straightforward.
When you execute the tests, you will see that it passes successfully:
Let's add two more test methods:
Assert.Throws
checks if the executed code throws a matching exception.
See the xUnit & Shoudly documentation to learn more about these libraries.
Classes With Dependencies
If your service has dependencies and if you want to unit test this service, you need to mock the dependencies.
Example: Testing a Domain Service
Assume that you've an IssueManager
Domain Service that is defined as below:
IssueManager
depends on the IssueRepository
service, that will be mocked in this example.
Business Rule: The example AssignToUserAsync
doesn't allow to assign more than 3 (MaxAllowedOpenIssueCountForAUser
constant) issues to a user. If you want to assign an issue in this case, you first need to unassign an existing issue.
The test case below tries to make a valid assignment:
Substitute.For<IIssueRepository>
creates a mock (fake) object that is passed into theIssueManager
constructor.fakeRepo.GetIssueCountOfUserAsync(userId).Returns(1)
ensures that theGetIssueCountOfUserAsync
method of the repository returns1
.issueManager.AssignToUserAsync
doesn't throw any exception since the repository returns1
for the currently assigned issue count.issue.AssignedUserId.ShouldBe(userId);
line checks if theAssignedUserId
has the correct value.await fakeRepo.Received(1).GetIssueCountOfUserAsync(userId);
checks if theIssueManager
called theGetIssueCountOfUserAsync
method exactly one time.
Let's add a second test to see if it prevents to assign issues to a user more than the allowed count:
For more information on the mocking, see the NSubstitute documentation.
It is relatively easy to mock a single dependency. But, when your dependencies grow, it gets harder to setup the test objects and mock all the dependencies. See the Integration Tests section that doesn't require mocking the dependencies.
Tip: Share the Test Class Constructor
xUnit creates a new test class instance (IssueManager_Tests
for this example) for each test method. So, you can move some Arrange code into the constructor to reduce the code duplication. The constructor will be executed for each test case and doesn't affect each other, even if they work in parallel.
Example: Refactor the IssueManager_Tests
to reduce the code duplication
Keep your test code clean to create a maintainable test suite.