Why Unit Testing?
Unit testing is an essential instrument in the toolbox of any serious developer. However, it can sometimes be quite difficult to know how to write unit tests for a particular piece of code. Having difficulty testing their own or someone else’s code, developers often think that their struggles are caused by a lack of fundamental testing knowledge or secret unit testing techniques.
What are the benefits of unit testing the code?
1. The BUGS: Any bugs in the code can be detected early with unit testing. By testing individual units of code in isolation, developers can easily identify and fix issues before they become harder to debug at deployment or production.
2. Code Quality Improvement: Since unit tests need to test the smallest possible module without many dependencies, developers are forced to write clean, modular, and well-structured code that is easier to read, maintain and debug. This helps in developing a higher code quality, reduced technical debt, and a more efficient development process.
3. Reducing Time and Costs: Finding and fixing bugs early can save developers significant time and costs in the long run. By catching bugs early on, developers can avoid time-consuming debugging and maintenance efforts down the line.
4. Ensuring code readability: Unit tests ensure that code behaves consistently and reliably which is extremely important in mission-critical applications.
5. Provides Documentation: Unit tests serve as documentation for the code, making it easier for developers to understand code working and its functionality. This can help reduce the time required for maintaining the code and for onboarding new team members.
6. Refactoring: Developers can make changes to the code with confidence that with comprehensive tests in place, the existing functionality does not break.
What is Unit Testing?
Unit tests are conducted individually. They’re often referred to as “test cases” consisting of segments of code that work together to perform a specific function. Each unit test evaluates the written code to ensure that it aligns with each specific function.
However, unit tests are more structural, meaning they don’t interact with any underlying APIs. Therefore, they don’t assess the user interface or any user-end functions.
Unlike most other types of testing, unit testing doesn’t involve the final user as the target audience, which is what makes each unit test unique. While these tests don’t focus on usability or any other non-functional aspects of an application, they still serve as a genuine authentication that the user requirements are met.
The quality of unit tests depends on the ability to foresee and correctly implement cases that should be in the test suite. A common practice is to add test cases that refer to specific errors identified during production usage. It makes sense to start implementing unit tests in critical parts of the application such as login or payment.
Unit tests are both written and read by developers. The entire point is to check and see if their coding works properly and document it as well as to increase the overall quality, reduce the cost of bug fixes and production failures.
Ultimately, unit tests let developers know whether or not their software applications are ready to use.
Unit Testing Rules
- It talks to the database
- It communicates across the network
- It touches the file system
- It can’t run at the same time as any of your other unit tests
- You have to do special things to your environment (such as editing config files) to run it
But, why should Unit Testing run in isolation?
When it comes to unit testing best practices, speed is the most important of them. Relying on things like the database or the file system slows tests down.
If a unit test is failing, it has to continue to fail until someone changes the code under test. The opposite is also true: if a test is currently passing, it shouldn’t start failing without changes to the code it tests.
If a test relies on other tests or external dependencies, it might change its status for reasons others than a code change in the system under test.
An ideal unit test should only cover a specific and small portion of the code because, when that test fails, it’s almost guaranteed that the problem happened on that specific portion of the code. That is to say, a proper unit test is a fantastic tool for obtaining super precise feedback.
If, on the other hand, a test relies on the database, the file system, and another test, the problem could be in any one of those places.
For example lets assume we are writing a code to build a spring boot application to perform CRUD operations. The simplest unit tets would be on the methods implemented in the service layer which interacts with the database.
But since unit tests should not be dependent on any other components and should work on the smallest module available, we will be mocking the repository dependency. We use Mockito and MockMvc for testing.
Mockito and MockMvc are two different tools that serve different purposes in Java development. Mockito is a popular Java mocking framework used for creating and managing mock objects in unit tests(Just like how we mock the repository). It helps simulate dependencies and isolate the code under test from external systems, making it easier to test and verify the behavior of the code in a controlled environment.
MockMvc, on the other hand, is a testing framework specifically designed for testing Spring MVC web applications. It provides a way to test the behavior of your controllers by sending HTTP requests and verifying the responses. MockMvc allows you to simulate a web application environment and test your controllers in isolation from the rest of the application.
In summary, Mockito is used for creating and managing mock objects in unit tests, while MockMvc is used for testing Spring MVC web applications. While they can both be useful in developing high-quality Java applications, they serve different purposes and are used in different contexts.
How to write a Unit Test?
As every program has a set of rules it need to follow, Unut tests also follow AAA pattern, which is Arrange, Act and Assert.
Let’s recap the rules, a unit test should test the smallest possible code without dependency and according to the above example, it is a service calling a function from the repository.
But since unit test should not be dependent on the repository we need to create values in the repository.
Lets see how unit test looks on a snippet of code.
@Service @Slf4j public class UserServiceImpl implements UserService{ private final UserRepository userRepository; public UserServiceImpl(UserRepository userRepository) { this.userRepository = userRepository; } @Override public List<UserEntity> getAllUsers() { return userRepository.findAll(); }
A unit test for getAllUsers() function with a repository dependencyand following the AAA pattern looks like the following.
public class UserRepoTest { @Mock private UserRepository userRepo; @InjectMocks private UserServiceImpl userServiceImpl; @Test void test_getAllUsers_shouldReturnAllUsers() { //Arrange List<UserEntity> users = new ArrayList<>(); UserEntity user1 = UserEntity.builder() .id(1L) .firstname("John") .lastname("Doe") .username("johndoe") .email("johndoe@example.com") .createdAt(Instant.now()) .lastModifiedAt(Instant.now()) .build(); UserEntity user2 = UserEntity.builder() .id(2L) .firstname("Jane") .lastname("Doe") .username("janedoe") .email("janedoe@example.com") .createdAt(Instant.now()) .lastModifiedAt(Instant.now()) .build(); users.add(user1); users.add(user2); //Act Mockito.when(userRepo.findAll()).thenReturn(users); List<UserEntity> actualUsers = userServiceImpl.getAllUsers(); //Assert assertEquals(users.size(), actualUsers.size()); } }
We use @Mock annotation from mockito frame tork to mock the repository and @InjectMocks for the service using the Mock, @Test is an annotation to specify to that the below code is a test. In the arrange section we arrange for the values that matches the repository and act is wher we perform the operation and while asserting we assert the values we get from the actual function call with the expected result.