Unit Testing – The Basics
Learn to unit test
Unit testing is about testing the smallest unit of code we can isolate from the rest of the code. Typically, this is a class or a method. We test this class in isolation from other classes. The point is to ensure the class works as intended in isolation, and remove any defects from it, before we integration test it with the rest of the code. This way when we integration test our code, we can focus on removing integration defects, since we already removed the unit defects. This reduces the noise unit defects cause that slow us down when we integration test and look for integration defects.
Typically, we create a tester class for the class we want to test. The tester class tests the visible methods of the class under test. In Java the visible methods are the public, protected, and default methods, but not the private methods. The unit tests are methods in the tester class that call the visible methods in the class under test to test different aspects of them.
Note we want all our test code to be physically separate from our application code. We want to be able to build and deploy the application code to production without the test code. In Java it is common practice to put all the application source code files in the src/main/
folder, and all the test source code files in the src/test/
folder. In .Net it is common to have all test source code in a completely separate project all together. While we physically separate test code from application code, it is common practice to put tester classes in the same name space as the classes they are testing. This way they are logically together. If our application has the class LoginService
in package org.example
, then its corresponding tester class LogingServiceTest
is also in package org.example
.
Please note, we will learn to unit test the Test-Driven Development (TDD) way in a future lesson. For now let’s get comfortable with what unit tests are in the first place.
So for example we may have the following class and method in our application that we want to test. Calling chengePassword(userAccount, newPassword)
changes the password in the user account to the new password.
public class LoginService {
public void changePassword (UserAccount userAccount, String newPassword) {
...
}
}
If we were unit testing the above class we would have the below corresponding tester class. This class would have methods representing our unit tests. These unit test methods test different aspects of the changePassword()
method above. Note how easy it is to tell what these unit tests are testing simply by reading their method names.
public class LoginServiceTest {
@Test
public void changePasswordFailsIfPasswordTooShort () {...}
@Test
public void changePasswordFailsIfOldAndNewPasswordsSame () {...}
@Test
public void changePasswordFailsIfPasswordHasNoUppercase () {...}
@Test
public void changePasswordSucceedsIfPasswordMeetsAllRules () {...}
}
Below is the implementation of one of the unit test methods from above.
@Test
public void changePasswordSucceedsIfPasswordMeetsAllRules () {
// arrange
LoginService loginService = new LoginService();
UserAccount userAccount = new UserAccount("user", "password");
String newPassword = "newPassword1";
// act
loginService.changePassword(userAccount, newPassword);
// assert
Assertions.assertEquals(newPassword, userAccount.getPassword());
}
Note we first arrange our test, meaning we get the app to the state we need for this particular test. In this case we need to create the class we are going to test, we need to create a user account with a prior password, and we need to define the new password we are going to try to set.
We then act, meaning we call the method we are testing. We run it with the input data we set up in the arrange section.
And finally, we assert, meaning we check that the state of our application is what we expect it to be after we call the method we are testing. In this case we assert that the expected result (newPassword
) equals the actual result (userAccount.getPassword()
). When an assertion fails, the unit testing framework reports the failed unit test to us, so we can fix our application code to get it to pass.
Exercises
Let’s practice coming up with different aspects of our changePassword()
method that we would want to unit test. We will practice two things: (1) coming up with aspects to test, where each aspect is one unit test, and (2) naming our unit tests in a way that describes what we are testing and easy to understand.
For example, we already have the following unit tests defined:
changePasswordFailsIfPasswordTooShort() | Asserts changePassword() fails if new password is less than six characters long. |
changePasswordFailsIfOldAndNewPasswordsSame() | Asserts changePassword() fails if new password and old password are the same. |
changePasswordFailsIfPasswordHasNoUppercase() | Asserts changePassword() fails if new password doesn’t have at least one uppercase letter. |
changePasswordSucceedsIfPasswordMeetsAllRules() | Asserts changePassword() succeeds if all the rules are met. |
Use the practice space below to define additional unit tests:
What other password rules would you want to enforce? For example:
- At least one number
- At least one special character
- No more than 16 characters long
- Can’t be the same as the last three passwords
What other behavior would you want to enforce? For example:
- If the password was changed then a log entry was created to log that event
- If the password was changed then it was added to the history of the last three passwords
Here are some possible unit tests you could define:
changePasswordFailsIfPasswordHasNoNumbers() | Asserts changePassword() fails if new password doesn’t have at least one number. |
changePasswordFailsIfPasswordHasNoSpecialChars() | Asserts changePassword() fails if new password doesn’t have at least one special character. |
changePasswordFailsIfPasswordLongerThan16Chars() | Asserts changePassword() fails if new password is longer than 16 characters long. |
changePasswordFailsIfPasswordSameAsLastThree() | Asserts changePassword() fails if new password is the same as one of the last three passwords. |
changePasswordLogsPasswordChangeEvent() | Asserts changePassword() logs the event that the password has been changed. |
changePasswordAddsPasswordToHistory() | Asserts changePassword() adds the password to the history of the last three passwords. |
Let’s practice implementing a unit test.
Remember it helps to structure a unit test method implementation in three parts. First we arrange the data, or in other words set the state to what it needs to be for the test. Then we act, which means execute the thing we want to test. Finally we make an assertion. The assertion is where we compare the actual result to the expected result and if they don’t match then we fail the test.
Here is the skeleton for our unit test method:
@Test
public void changePasswordFailsIfPasswordTooShort () {
// arrange
// act
// assert
}
Here are all the line of code for our implementation of changePasswordFailsIfPasswordTooShort()
, but they got scrambled and are out of order. See if you can put them in the right order. Consider which lines belong in the arrange, act, and assert sections of our unit test implementation.
Assertions.assertEquals(oldPassword, userAccount.getPassword());
String newPassword = "pass";
UserAccount userAccount = new UserAccount("user", "password");
loginService.changePassword(userAccount, newPassword);
String oldPassword = userAccount.getPassword();
LoginService loginService = new LoginService();
Here is the implementation of our unit test using the lines of code from the Hint tab:
@Test
public void changePasswordFailsIfPasswordTooShort () {
// arrange
LoginService loginService = new LoginService();
UserAccount userAccount = new UserAccount("user", "password");
String oldPassword = userAccount.getPassword();
String newPassword = "pass";
// act
loginService.changePassword(userAccount, newPassword);
// assert
Assertions.assertEquals(oldPassword, userAccount.getPassword());
}