Tutorial 5 Unit Testing (JUnit,AssertJ,Mockito) - McGill-ECSE429-Winter2022/tutorials GitHub Wiki
Let's say you have an amazing application that's running well, but you'd like to make some changes to it. Without techniques such as JUnit this would be a very dangerous thing to do. Writing unit tests is very different from writing regular code. We'll start with the heart of JUnit, annotations. Then we'll move on to assertions. We also dive into some more advanced topics such as (indistinct) and running test parallel.
Basic Java and experience working with an IDE.
Unit testing is a software testing method by which individual units of source code—sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures—are tested to determine whether they are fit for use.
- Validate the smallest unit of software.
- Find bugs easy and early.
- Save time and money.
- Can force developers to write better and cleaner code.
Quiz:
- Why do we need more testing than just unit testing?
- What does unit testing do for code quality?
- What are the advantages of TDD for your software?
Note: Be aware that, only performing unit testing will not catch all the bugs.
- Unit testing framework for java.
- Part of the xUnit series.
- Enables automated unit testing.
- Must-have for TDD.
Assumption: Eclipse and SDK is installed.
Steps:
- Create a new project.
2. Update the properties and add the following dependency.
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>16</maven.compiler.source>
<maven.compiler.target>16</maven.compiler.target>
</properties>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.7.2</version>
<scope>test</scope>
</dependency>
3. Test setup.
Please make sure your test runner is set to Junit5.
- Write some code that we can test.
- Add test class.
- Write test.
- Run test.
Add a class named Code and copy-paste the following code.
"paste the code block here for Code"
Add a test class named _CodeTest _ and copy-paste the following code.
"paste the code block for test
Run the test and should see the test passes.
Excercise: Do it for the failure flow.
- Starts with @
- JUnit uses them a lot
- Source code metadata JUnit Annotations is a special form of syntactic meta-data that can be added to Java source code for better code readability and structure.
Example: we have already seen an example of annotation in our previous exercise of CodeTest. Some of the popular annotations are:
@Test, @Before, @BeforeClass, @After, @AfterClass and _@Ignores_. There are many more others, I'll let you explore this part.
Create a class BankAccount and copy-paste the following code:
public class BankAccount { private double balance; private double minBalance;public BankAccount(double balance, double minBalance) {
this.balance = balance;
this.minBalance = minBalance;
}
//Getter
public double getBalance() {
return balance;
}
public double getMinBalance() {
return minBalance;
}
//methods
public double withdraw(double amount) {
if(balance-amount > minBalance) {
balance -= amount;
return balance;
}
else {
throw new RuntimeException();
}
}
public double deposit(double amount) {
balance += amount;
return balance;
}
}
Create a Test class BankAccountTest and copy-paste the following code:
public class BankAccountTest {
@Test
public void testWithdraw() {
BankAccount bankAccount = new BankAccount(500, -1000);
bankAccount.withdraw(300);
assertEquals(200, bankAccount.getBalance());
}
Run the test.
Exercise: Trigger the exception. Write a test method for deposit and check both success and failure scenarios.
- Improves the report.
- Adding a display name for a test.
- How? using an annotation!
So we have written our first unit tests already. That's great. Let's inspect how we can improve the report that is coming out of this test using the display name. Display name is an annotation that allows us to replace a default name with a custom name. This allows us to make the report more descriptive.
Exercise:
Add the following code under @Test annotation of your test class and execute the test. Identify the difference.
@DisplayName("Withdraw 500 successfully")
@DisplayName("Deposite 500 successfully")
- Check the outcome of the test.
- If the assertion fails, the * test fails.
- Assertion class in org.junit.jupiter.api package.
- Many assertions available.
Assertions are used to check a certain condition. If an assertion fails, the test fails. We can find the assertions in the assertions class in the org.junit.jupiter.api package. We usually use a static import for this one so that we can use all the methods without having the assertions. in front of them. There are very many methods inside this class, and they're all used to perform the actual tests. We have seen one already, assertEquals. you can find full list of assert statements here.
Exercise:
Add the following code into your BankAccountTest class:
public void testWithdrawNotStuckAtZero() {
BankAccount bankAccount = new BankAccount(500, -1000);
bankAccount.withdraw(500);
assertNotEquals(0, bankAccount.getBalance());
}
Run the test.
Show demo on isActive bank account and accountHolderName. (assertTrue & assertNotNull).
Assumptions are a way of setting conditions for executing a test. If we meet the assumption, the test will get executed. If we don't meet the assumption, the test won't be executed. Assumptions are in a class in the org.junit.jupiter.api package. This class contains methods that will test assumptions. The main difference between assertions and assumptions is that a failed assumption does not lead to a failed test but to an aborted test instead.
Exercise: Follow the demo on the previous assertTrue example. (assumeTrue, bankAccount is not null).
Create a new test class BankAccountOrderedExecution inside test and paste the following code:
public class BankAccountOrderedExecution {
static BankAccount bankAccount = new BankAccount(0, 0);
@Test
public void testWithdraw() {
bankAccount.withdraw(300);
assertEquals(200, bankAccount.getBalance());
}
@Test
public void testDeposit() {
bankAccount.deposit(500);
assertEquals(500, bankAccount.getBalance());
}
- Run the test. And Analyse the issue. Run for one method should fail.
Now update the code with the following snippet and run it again.
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class BankAccountOrderedExecution {
static BankAccount bankAccount = new BankAccount(0, 0);
@Test
@Order(2)
public void testWithdraw() {
bankAccount.withdraw(300);
assertEquals(200, bankAccount.getBalance());
}
@Test
@Order(1)
public void testDeposit() {
bankAccount.deposit(500);
assertEquals(500, bankAccount.getBalance());
}
}
- Uses to control the relationship between tests.
- Useful when: feature separation; code organized around method or feature.
We have just seen test execution and how we can influence the order of our tests. With nested tests, we can control the relationship between tests. This can, for example, be useful when you would want to separate a certain feature or a specific method and organize your code around it. It is often also used to execute, in case of certain conditions, such as when you, when something is not found, when something is found, and for example, when an exception gets thrown.
Exercise: Create a test class BankAccountNested and use the following code:
public class BankAccountNested {
@Test
public void testWithdraw() {
BankAccount bankAccount = new BankAccount(500, -1000);
bankAccount.withdraw(300);
assertEquals(200, bankAccount.getBalance());
}
@Test
public void testDeposit() {
BankAccount bankAccount = new BankAccount(500, 0);
bankAccount.deposit(500);
assertEquals(1000, bankAccount.getBalance());
}
@Nested
class WhenBalanceEqualsZero {
@Test
public void testWithdrawMinimumBalanceIs0() {
BankAccount bankAccount = new BankAccount(0, 0);
assertThrows(RuntimeException.class, () -> bankAccount.withdraw(500));
}
@Test
public void testWithdrawMinimumBalanceNegative1000() {
BankAccount bankAccount = new BankAccount(0, -1000);
assertEquals(-500, bankAccount.withdraw(500));
}
}
}
Run the test.
- Less tightly coupled classes.
- Separation of concerns.
- No need to manually create the instance; we get it handed to us.
- In our case, no new BankAccount(0,0) anymore! Dependency injection is a way of having less tightly coupled classes, which is a good thing. Definitely for tests because it encourages separation of concerns even more, which is what we want when we're only testing units. Dependency injection occurs when we don't need to create the instance but we just get it handed to us. Dependency injection can be done in several ways on the field of a class but also on the method and the constructor parameters.
Example: In the figure you can see an example of some pseudocode with dependency injection and without dependency injection. First, we have the without dependency injection. We have some sort of class, a car, and this car, it has an engine, and we need to instantiate this engine, either here or in the constructor or whenever we want to use it first. Then we have the rest of the code of the class. With dependency injection, we actually have the @Inject annotation, and the framework will be giving us our Engine object. So no need to instantiate it before moving on with the rest of your code. And this is on the field level but this can also be done on the method and on the constructor level.
- Use @RepeatedTest.
- Great for executing multiple times and testing the responses after the first time.
We can repeat a test a certain number of times. You might guess how, using an annotation of course. We use the @RepeatedTest annotation for this, and this is great for several reasons. For example, testing the behaviour of an endpoint when you access it multiple times.
Exercise: Create a test class BankAccountRepeatedTest and use the following code.
public class BankAccountRepeatedTest {
@Test
@RepeatedTest(5)
public void testDeposit() {
BankAccount bankAccount = new BankAccount(500, 0);
bankAccount.deposit(500);
assertEquals(1000, bankAccount.getBalance());
}
}
We've just learned how to repeat a test a certain number of times. This becomes way more interesting, combining it with parameterized tests. Using the @ParameterizedTest annotation enables us to specify a data source for our parameters. And when we want to run the test, a certain number of times, we can do so with different parameters.
Exercise: Create a test class called BankAccountParameterizedTest and copy-paste the following code:
public class BankAccountParameterizedTest {
@Test
@ParameterizedTest
@ValueSource(ints = {100, 200, 500, 800})
@DisplayName("Depositing successfully")
public void testDeposit(int amount) {
BankAccount bankAccount = new BankAccount(0, 0);
bankAccount.deposit(amount);
assertEquals(amount, bankAccount.getBalance());
}
}
Many cases, you would want to do something before and after your tests. We can perform certain actions before and after our test methods using, well, of course, annotations.
Exercise: Follow on-screen demo.
Reference for more exploration: here.
The AssertJ project provides fluent assertion statements for test code written in Java. These assert statements are typically used with Java JUnit tests. The base method for AssertJ assertions is the assertThat method followed by the assertion.
Please add the following dependency in your POM file:
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.20.2</version>
<scope>test</scope>
</dependency>
Please follow along with the demo on screen: detailed tutorial
Step 1. Create a class that extends AbstractAssert. Use the following code:
public class PlayerAssert extends AbstractAssert<PlayerAssert, Player> {
public static PlayerAssert assertThat(Player player){
return new PlayerAssert(player);
}
public PlayerAssert(Player player){
super(player, PlayerAssert.class);
}
public PlayerAssert hasName(String expectedName){
isNotNull();
if(!actual.getName().equals(expectedName)){
failWithMessage("Expected name: " + expectedName + ", but was: " + actual.getName());
}
return this;
}
}
Step 2. Create a static assertThat method returning the PlayerAssert class.
public static PlayerAssert assertThat(Player player){
return new PlayerAssert(player);
}
Step 3. Create a customer matcher method that returns PlayerAssert. Call isNotNull() to make sure the argument is present and use failWithMessage() to cause the test to fail following the logical test.
public PlayerAssert hasName(String expectedName){
isNotNull();
if(!actual.getName().equals(expectedName)){
failWithMessage("Expected name: " + expectedName + ", but was: " + actual.getName());
}
return this;
}
Step 4. Call the customer assertThat created in your test class and run the test.
public class PlayerStatisticsTestCustomMatcher {
@Test
public void playerConstructorAssignsName(){
Player player = new Player("Stuart", 30);
PlayerAssert.assertThat(player).hasName("Stuart");
}
Whenever you are using any framework, you'll often find yourself using common libraries that go well with it. For unit testing, you really want to minimize the dependencies. This is the point where it's time to meet the Mockito library. Mockito is a testing library that helps stubbing and mocking objects. "Mocking" means that we're going to create an isolated version of the object that we can influence, rather than the real one. This will help us to keep our dependencies to a minimum, and isolate our test to be truly testing the simple unit.
Add the following block of code to your dependency file (POM).
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>2.23.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.8.0</version>
<scope>test</scope>
</dependency>
Update the project.
Mockito provides several methods to create mock objects:
- Using the @ExtendWith(MockitoExtension.class) extension for JUnit 5 in combination with the @Mock annotation on fields
- Using the static mock() method.
- Using the @Mock annotation.
Create a test class CalculateMethodMockitoTest and add the following code in your class:
@ExtendWith(MockitoExtension.class)
public class CalculateMethodMockitoTest {
@Mock
CalculateMethods calculateMethods;
@BeforeEach
public void setupMock() {
Mockito.when(calculateMethods.divide(6, 3)).thenReturn(2.0);
}
@Test
public void testDivide() {
assertEquals(2.0, calculateMethods.divide(6, 3));
}
}
Now run the test. It should pass, change the value for mockito return to 1.0 and then again run the test, validate the result it should fail now.
Note: As a prerequisite, you need to write a sample code for calculateMethod class in your java source folder. Initialize the constructor and define a method divide.
Reference: here