A unit test is responsible for testing a unit of work. A characteristic of these tests is that they try to test a small and isolated part of the software, without caring about external dependencies.
Unit tests have the peculiarity of being quite fast, so it is normal to see test suites composed of a high proportion of unit tests.
In practical terms, a unit test tends to test a function of a class. It is normal to write several unit tests which verify different behaviors of a function.
Example. Suppose we have software to make fund transfers between bank accounts. We want to do unit tests to a function of our application. First let’s see a model of our source code:
public class Account { public decimal Funds { get; set; } }
This is a simplified model of a bank account, which only has the information of the funds in the account, that is, the money that is currently in that account. Now, let’s see the function that is responsible for making transfers between accounts:
public class TransfersService { public void TransferBetweenAccounts(Account from, Account to, decimal amount) { if (amount > from.Funds) { throw new ApplicationException("The origin account does not have enough funds for the operation"); } origin.Funds -= amount; destination.Funds += amount; } }
This function is used to transfer funds between accounts, as parameters receive an account of origin, a destination account, and an amount to transfer. If the source account does not have sufficient funds to perform the transaction, an error is thrown. Certainly this is a fictitious function which simplifies the transfer of amounts between accounts, however, it is enough to be able to create two unit tests.
Personally, I prefer to start with negative tests, that is, tests that in one way or another verify that invalid operations receive an adequate error message. In our case, a negative test would be to try to transfer funds from a source account without sufficient funds, and verify that the user receives an appropriate error message. Let’s do a unit test to verify this. The first thing we need is to create a test project, for that:
– If you use Visual Studio, you must right click on your solution > Add > New Project. Then click on Test and choose the option Unit Test Project. Put a name to the project, and press OK.
– If you are using the dotnet CLI, you can use the dotnet new mstest -n [Project Name] command to create a test project.
Then, create the following class in that project:
[TestClass] public class TransfersServiceTest { [TestMethod] public void TransferBetweenAccountsWithInsufficientFundsThrowsAnException() { // Prepare Exception expectedException = null; Account origin = new Account() { Funds = 0 }; Account destination = new Account() { Funds = 0 }; decimal amount = 5m; var service = new TransfersService(); // Test try { service.TransferBetweenAccounts(origin, destination, amount); Assert.Fail("An exception should have been thrown"); } catch(Exception ex) { expectedException = ex; } // Verify Assert.IsTrue(expectedException is ApplicationException); Assert.AreEqual("The origin account does not have enough funds for the operation", expectedException.Message); } }
In the previous code we have a class with a method. The class and the method are decorated with the attribute TestClass and TestMethod, respectively. A TestClass is a class that we use to group several tests. A TestMethod is a method which we use to test functionality.
We can see that the previous TestMethod is divided into the 3 parts we had talked about in the previous post: preparation, testing and verification. In the preparation part we create the accounts, the amount to be transferred and initialize the class that we will use to make the transfer. In the test phase we simply execute the functionality we want to test. In our case, we put it in a try block to catch the exception. Finally, in the verification stage, we verify that the exception thrown is of the ApplicationException type, and that the message of the exception is correct. The checks are made by making Asserts.
Assert, which means to affirm, helps us to make statements which we understand must be correct, if not, an error is thrown and the test fails. A test is successful when all your assertions are accurate. In the previous example, we affirmed that the exception was of the ApplicationException type and that the error message was correct.
We can run this test in different ways. If you are using Visual Studio, you can go to Test > Windows> Test Explorer, and press Run All. This will cause all the tests found in the solution to run.
We can see that the test was successful
If you want, you can run the tests of your solution using the dotnet CLI. For that, use the dotnet test command in the folder where your test projects are located.
Let’s now do a test where we verify that the operation is successful if there are sufficient funds:
[TestMethod] public void TransferBetweenAccountsEditTheFunds() { // Prepare Account origin = new Account() { Funds = 10 }; Account destination = new Account() { Funds = 5 }; decimal amount = 7m; var service = new TransfersService(); // Test service.TransferBetweenAccounts(origin, destination, amount); // Verify Assert.AreEqual(3, origin.Funds); Assert.AreEqual(12, destination.Funds); }
In this second test the transaction is successful, so our verification consists of verifying that the funds were debited from the origin account, and credited to the destination account.
Now that we have the two test methods, you may see that we can do a small refactoring: We can centralize the instantiation of the TransfersService class in a common place. The idea of this is that, if one day the TransfersService constructor changes, we should only change one place, instead of having to change several places. Good software development principles also apply to your automatic tests. In this case, we talk about the principle of decreasing the repeated code whenever it is viable.
We are going to alter the TransfersService constructor to accept a parameter. This parameter will be a class which will encapsulate the validation rules of a transfer. Thus, the TransfersService class will focus on the transfers, and the validation of these will be delegated to another class. This serves to respect the principle of single responsibility. Let’s suppose that we have the following code:
public interface ITransferValidationService { string DoValidations(Account origin, Account destination, decimal amount); } public class TransfersService { private readonly ITransferValidationService _transferValidationService; public TransfersService(ITransferValidationService transferValidationService) { _transferValidationService = transferValidationService; } public void TransferBetweenAccounts(Account origin, Account destination, decimal amount) { var errorMessage = _transferValidationService.DoValidations(origin, destination, amount); if (!string.IsNullOrEmpty(errorMessage)) { throw new ApplicationException(errorMessage); } origin.Funds -= amount; destination.Funds += amount; } } public class TransferValidationServices: ITransferValidationService { public string DoValidations(Account origin, Account destination, decimal amount) { if (amount > origin.Funds) { return "The origin account does not have enough funds for the operation"; } // ... other validations return string.Empty; } }
If we compile our project, we will have two errors that come from our automatic tests. What happens is that now our TransfersService class requires a parameter in the constructor. However, we must ask ourselves a question, from our automatic tests, what should happen to the class we want to test? Should we pass TransferValidationServices?
The answer to this question depends on several factors. If for some reason we know that there will only be one class that implements ITransferValidationService, then we may not even want to use an interface, and we prefer to simply pass the TransferValidationServices class as a parameter. In this case, we can pass TransferValidationsService from our automatic tests to TransfersService.
However, there are times when this may not be ideal. If you plan to have several implementations of ITransferValidationService, or if you want to test the TransfersService class without using its dependencies, then you must not pass TransferValidationServices from your automatic tests to TransfersService, but you must pass a mock. We’ll talk about mocks in the next post.
Summary
Unit tests verify a unit of work. Typically this means testing a function, although there may be several. Regarding the issue of dependencies, it is not always obligatory to have to inject the dependencies of a class that we are going to test, especially if these dependencies are part of the unit of work that we want to test.
Other entries of this series:
- Basic Concepts of Automatic Testing
- Fundamentals of Unit Tests (current entry)