Software test automation refers to using software tools to automate the execution of tests and compare actual results with expected results, resulting in more efficient and accurate testing processes. There are multiple tests, including unit testing, integration testing, and end-to-end (E2E) testing. In this article, we will focus on unit tests.
Unit testing involves testing individual units or components of the software in isolation from the rest of the system to ensure that it performs as expected and meets its design requirements. They use test cases that include a set of inputs and expected outputs, and they can be run automatically and frequently to ensure that changes to the code do not introduce new defects.
For the next part of this article, we need a functional nestjs project, in a previous article we built a Rest API that handles users, we will use the same project, and you can find it here.
Once you have the project cloned and open in your favorite code editor, we need to install dependencies by running the following command:
In the introduction, we mention that software automation refers to using software tools to automate testing, according to the project language and the type of tests you want to write there is a multitude of packages that can be used.
For this article we are going to use Jest, because is the most popular one also when you create a new nestjs project, Jest is installed by default, so we don’t need to install other dependencies.
Unit tests writing
Since we used a framework, we can assume that its internal working is well tested and then as a developer we are responsible for testing the code that we have written. For the project that will be ours during this article, we can notice that the code written by the developers is focused on the
users.service so we need to write unit tests for each of them.
Testing the service layer
When using the nestjs cli to generate a new resource, it generates files like the
users.controller.ts for each file, it also generated a file with the same name but with
.spec.ts as extension, those files are for writing tests. So when you wanted to write a unit test you can edit those files instead of creating a new file on your own.
Let’s check what is the
As you can see, we have a bunch of code that might not be familiar to you, we are going to explain every part of this code before moving forward.
describe annotation is used to group related test cases and provide a description of what the group of tests is intended to test in our case we want to test the UsersService class. Then it declares a new variable
service of type
UsersService it’s an instance of the class we want to test.
BeforeEach is an annotation that will allow us to run a bunch of operations, before each test, it’s useful if you have data that are mandatory for every test instead of rewriting the same code in every test case you can write that logic in a
BeforeEach. The code above creates a
The Testing Module is provided by
@nestjs/testing it’s similar to the
users.module.ts that we have in the project. Its goal is to provide every dependency that we need to create an instance of
To create a new Testing Module, we use the
Test.createTestingModule which is also provided by
@nestjs/testing that takes as parameters the dependencies, providers, controllers, and so on.
Once the Testing module is created we retrieve an instance of the
UsersService class from the module and then we assign that to the variable declared above.
Here there is an important thing we need you to pay attention to, when you create a testing module, you need to provide, the class for which you are writing the tests and all dependencies that are injected in that class. The default createTestingModule receives only one parameter
UsersService itself but the
userRepository is missing, which means Nest can’t resolve the dependencies to create a new instance, to verify it, you can run the command
npm run test you will receive the error below
As you can see Nest can’t resolve dependencies, that means before moving forward we need to provide all dependencies that are needed to create an
it annotation (also called a test case) is used to define an individual test case.
Now we have reviewed the test file’s default content, we need to edit that according to our needs.
Let me show you the
users.service.ts code for which we will write tests.
As you can see, we have a dependency injected by using dependency injection and a bunch of methods, so writing unit test for this class, means we need to write tests for each method.
I have a few steps that I follow before writing tests, I suggest you to do the same:
First I check if the class for which I'm writing tests interacts with external dependencies, it’s simple to know that, you have just to check if there is dependency injection in the constructor, in this case, we have
@InjectRepository(User) private userRepository: Repository<User> that means we need to mock that dependency before moving forward.
Second, I create a test case for every method inside the class, to insure I don’t forget to test something.
Mocking external dependencies: mocking is a technique used in software testing to create fake or substitute objects that mimic the behavior of real objects or dependencies. This is done to isolate the code being tested from its dependencies so that it can be tested in isolation and with greater control.
users.service class, we have to mock the
userRepository class with its method.
We have created an object named
mockUserRepository that has attributes named in the same way as the method we want to mock. Every method receives
jest.fn() that creates a new, empty mock function to mimic the behavior of a real function.
Once the mock object is created we need also to update the Testing module creation.
Since we have mocked all external dependencies and provided them if you run again the command
npm run test the test will pass.
Creating test cases for each method: usually, I create test cases for each method.
If you have followed every step with me, your test file should look like this. We have all dependencies set, and all test cases created, then we can finally write the test code.
While writing tests, I always follow the Arrange-Act-Assert pattern, it’s a pattern that helps to make your tests maintainable and understandable.
Let’s see what it looks like:
The Arrange-Act-Assert pattern suggests you to break your test into three parts:
- Arrange: In this part, you arrange all data that you will need for your test, for the
UsersService.createmethod, as you can see it receives a parameter of type
CreateUserDtoso we need to create a variable for it. the create method also returns a user after calling a method of an external dependency, so we need to create the returned data and then we need to mock the result of that external method, remember we want to test that method in isolation which means you can’t refer to an external dependency. To mock the result returned by the external method we use
- Act: in this part, we call the function we are testing by giving it the data created in the arrange part, we also store its result.
- Assert: In the last part we can make our assertions, the goal is to check if the result is equal to what we expect and if the function behaves as we want. First, we check if the mocked function has been called, then we check if it has been called with the right data and finally we check if the result is equal to the mocked value.
Overall, our test for the
UsersService.create method, helps us to be sure that the method behaves as expected, we are sure that the
create method of the external dependency will always be called, it will be called with the data received as a parameter by the
UsersService.create and if everything goes well, we will receive a brand new user with an id.
Now we can do the same for the remaining methods
Your final code should look like the code above, we have used the same pattern for each method. Now if you run the
npm run test commande you will the result below:
Our six tests have passed 🔥
Testing the controller layer
Testing the controller layer is almost the same as testing a service class, the main difference here is when we try to create a Testing Module, take a look at the default code in the
As you can see on line 10, we have an attributed name
controllers that takes an array of controllers, and another attribute named provider that takes an array of providers. Here we need to do the distinction because we are testing a controller and every class that behaves as a controller is always prefixed by the
@Controller annotation but the remaining stay the same, we have an external dependency here
UsersService that we need to mock as we already did in the previous part.
We have created a mocked object and provided it as a dependency to create the testing module. Now let’s create test cases for each method.
We have created tests for the
UsersController methods by always mocking external dependency and using the AAA pattern to ensure maintainability and readability.
Now if we run the
npm run test command we have:
Twelve tests have passed.
You can also check the code coverage by running
npm run test:cov . Test coverage is a measurement of how much of your code is being tested by your automated tests. It measures the percentage of code that is executed during a test suite run.
Throughout this article, we have learned a bunch of things, the definition of software automation and unit testing, how to unit test a class, by mocking its external dependencies, test coverage, and some cool annotations provided by Jest.
You can find the final code here.
I hope you have enjoyed reading this article as I enjoyed writing it for you.