Table of contents:
- Introduction
- Test Driven Development with Java and JUnit
- How To MOCK with MOCKITO in JAVA
- Testing Spring Boot Controller
- Testing Spring Context
- TDD Java Best Practices
TDD is a simple 3-step process.
- Red: write a test, watch it fail.
- Green: write just enough code to pass the test.
- Refactor: improve the code without changing the behavior.
- They allow you to refactor safely
- They allow you to add features safely
- They act as an API Specification
- They act as documentation
- They facilitate team development
All developers already test their code!
- Developer has a problem.
- Developer writes code.
- Developer manually tests code, or deploy and manually test functionality.
- Repeat
Although this process might not take long (couple of minutes), it adds up. Ideally, an automated test should take milliseconds to run. On bigger projects, the time saved from automated testing more than makes up for the time writing the tests. This increases productivity.
Tests are a contract for expected behavior
Writing Tests:
- Broadcasts defined expected behavior to team.
- Broadcasts defined error-handling for edge cases.
- Warns other developers when they break your code.
Without Tests:
- How would you know if code gets broken?
Without tests, broken code is discovered at runtime… a waste in time and resources.
- Write no production code except to pass a test.
- Write only enough of a test to demonstrate a failure.
- Write only enough production code to pass a test.
-
Any code that can’t be tested cannot be trusted.
-
All code should be tested.
-
This could lead to uncomfortable scenarios.
-
This is what makes TDD hard!
- Tests should be small in scope
- Tests should only test one edge case or scenario
- This isolates bugs so you can easily see where they occur
- Write and pass each test one at a time.
- Write the EASIEST solution to pass a test (don’t future proof).
- If you want to write more production code, write another test!
- Write just enough code to get the job done - code is EXPENSIVE to maintain.
- Automates the testing process
- Fast Feedback
- Manageable code
- Adding new features is easier
- Tests act as API Specification
- Errors are quickly isolated
- Facilitates team development
- High Learning Curve (shift in thinking)
- Increased Development Time
Every test can be clearly written like this:
@Testannotation- Descriptive Test Name
- Test Body (3 Phases):
- ARRANGE - Initial Conditions
- ACT - Method to Test
- ASSERT - Verification of Result
@Test <1>
public void getBalance_ReturnsZero_WhenNoTransactions() { <2>
// ArrangeAccount account = new Account(); <3>
// Act
Long balance = account.getBalance(); <4>
// Assert
assertEquals(0L, balance); <5>
}
- Annotation - Informs the project that this is a test method.
- Descriptive Test Name - - For readability.
- Arrange - Instantiating a new account
- Act - Calling the method we want to test
- Assert - Verify the condition of the method we called we are testing. The value you expect to get back should be first, following with the actual result from act.
This the cycle that we use to make sure we are doing TDD properly.
We initially want to make a test be red, or failing, to verify our test is not giving us a false positive. If our test was initially passing in the beginning, but are trying to make it pass with our implementation, how do we know if our implementation is even proper to begin with?
There are two types of Red stages people know, the compiler error with a red squiggly line and an Assertion Error where the test is failing as shown below. For this exercise and for CDE Studios, we agree that the Red stage is where the test fails.
After we have a proper red stage, we now have TDD’s permission to code in the implementation class. We will not change the test code to make the test pass. Our implementation should make the test pass. We also want the simplest solution to pass our test. When we run the test, we should see a green arrow given to us as shown below. It is also very good practice to verify that all tests are passing, to fix anything, before continuing.
This is only done when we want to or find a need to refactor our code. We find that some solutions can be done more simple or we need to redesign our implementation. Before refactoring, make sure that all tests are passing. During the refactoring process, you will naturally break some of the code. This is okay. We are officially done refactoring when all the tests are passing again.
Tests should be written using only the same API that a user will have access to. This is called black box testing.
-
Black box testing ensures the expected result is obtained from predetermined initial conditions. It doesn’t matter how the result is achieved!
-
As you add more tests, you further define the algorithm.
TIP
Your tests define behavior, but they shouldn’t "be aware" of internal implementation. They only need to be aware of what you need to put in your method or what you expect to get out of that method.
- When a test calls on a private member or relies on internal logic not known to a user, you have COUPLED A TEST TO IMPLEMENTATION CODE.
WARNING
Since coupled tests are tied to the implementation code, refactoring usually breaks those coupled tests.
If you refactor a class to produce the same results, but it still breaks 40 tests, then the tests were most likely coupled to the implementation.
Mockito is a library for Java that simplifies dependency injection and testing.
- Mockito allows you to define behavior!
- Mockito allows you to verify when MOCKED calls have been made.
- Mockito allows you to spy on internal components.
- All without extending or implementing!
It allows a developer to easily stub a dependency for testing purposes.
Verify version with your team
testCompile group: 'org.mockito', name: 'mockito-core', version: '2.1.0'
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<!-- Verify version with your team -->
<version>2.1.0</version>
<scope>test</scope>
</dependency>
Mockito quickly allows you to define the behavior of a dependency.
- To mock a dependency you first have to declare and initialize it.
- Once you have initialized the mock, it can be treated like you would any other object.
- All methods for a mock by default return either null, the default value for a primitive, or an empty collection, as appropriate.
- Use
Mockito.when(…)to define behavior.
- Mockito.when only works with mocks! Attempting to use Mockito.when with a real instance will throw an error.
TwentySidedDie die = Mockito.mock(TwentySidedDie.class);
System.out.println("Value before Mockito.when: " + die.roll());
Mockito.when(die.roll()).thenReturn(20);
System.out.println("Value after Mockito.when: " + die.roll());
Value before Mockito.when: 0 Value after Mockito.when: 20
- After you setup the mock, you can pass or inject it to other classes.
- Another powerful feature of Mockito is Mockito.verify, which is used to determine if a method has been called a specified number of times.
- If you try to verify a method belonging to a real instance, you will get an Exception.
//Verify die.roll() was called 1 time
Mockito.verify(die, times(1)).roll();
//Alternative way to Verify die.roll() was called 1 time
Mockito.verify(die).roll();
//Verify die.roll() was never called
Mockito.verify(die, times(0)).roll();
//Verify die.roll() was called twice
Mockito.verify(die, times(2)).roll();
To use a mock, you must set up all 4 phases in any combination:
- Declare a variable as a mock
- Initialize the mock
- Define the behavior
- Inject the mock
Declaring a Mock
@MockObject object = Mockito.mock(Object.class);
Instantiating a Mock: If you do not instantiate, you will get a NullPointerException
Object object = Mockito.mock(Object.class);MockitoAnnotations.init(this);@RunWith(MockitoJUnitRunner.class)
Defining Behavior for a Mock
when(object.method()).thenReturn(value);
Injecting a Mock into a parent class.
@InjectMocks- compiler injects with constructor, setter, or by reflection- Constructor or Setter
When Spring @autowired dependencies:
- Spring searches for defined beans and "puts them in a bag".
- When Spring encounters a dependency, it "searches for a bean from the bag".
- It then injects the bean into the parent class.
- The developer can’t inject a mock because Spring injects everything!
- Spring injects only with the beans it has defined.
- Developer doesn’t call constructor or setter.
-
Use
@MockBean -
@MockBeantells Spring to setup a mock and inject it instead of a real instance. -
@MockBeanshould be used in place of@Autowired -
Mock can then be used as normal
@Service
public class ParentService{
@Autowired
private DependencyService;
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class ServiceTest(){
@Autowired FooService service;
@MockBean DependencyService dependency;
@Test
public void test(){
//Define behavior like normal
when(dependency.method()).thenReturn(value);
//call method to test
service.methodToTest();
}
}
@InjectMocks and @Mock WILL NOT WORK.
A Controller’s methods can be tested in the same way that any other class can be using JUnit and method-focused TDD.
@RestController
@RequestMapping("/services")
public class SomeEndpoint { (1)
@GetMapping("/hello")
public String hello() { (2)
return "hello";
}
}
-
Class Under Test
-
Method Under Test
Below is the corresponding JUnit Test
public class SomeEndpointTest {
@Test
public void hello() {
SomeEndpoint someEndpoint = new SomeEndpoint(); (1)
assertEquals("hello",someEndpoint.hello());(2)
}
}
-
Instantiate Controller
-
Invoke and check method
Controllers only become special when certain annotations are added supporting Spring in adding additional behaviors. To test that this configurations are working as expected, Spring Context Integration Tests can be used to verify the following;
-
exposure of endpoints is as expected
-
payloads are being processed correctly and handed to the correct methods
-
responses and exceptions are managed correctly
To perform Spring Context Integration Testing correctly, we invoke the exposed endpoints via a client. This allows us to simulate a remote client making requests of our service. The main library that supports doing this is Spring’s MockMvc.
@RestController
@RequestMapping("/services") (1)
public class SomeEndpoint {
@GetMapping("/hello") (2) (3)
public String hello() {
return "hello"; (4)
}
}
-
Root endpoint under test
-
GETmethod under test -
URL under test
-
Expected response
Below is an integration test for the above code
@RunWith(SpringRunner.class) (1)
@SpringBootTest (2)
@AutoconfigureMockMvc (3)
public class SomeEndpointIT { (4)
@Autowired (5)
MockMvc mockMvc;
@Test
public void testGetHello() throws Exception { (6)
mockMvc.perform(get("/service/hello")) (7)
.andExpect(status().isOk()) (8)
.andExpect(content().string("hello")); (9)
}
}
-
Annotation - tells JUnit to work with Spring
-
Annotation - sets up the Spring Application Context
-
Annotation - sets up a
MockMvcobject (server for our method) -
Class - Test class differently named to our Unit Tests
-
Annotation - tells Spring to inject a copy of our server into the test
-
Method - using
MockMvcit will throw an exception on failure and we need to throw it up -
Method -
MockMvc"performs" theGEToperation against our URL just like a client would -
Test -
MockMvc"expects" that the HTTP Status Code will be200/OK -
Test -
MockMvc"expects" the reply from the server to have only the wordshelloin it
MockMvc runs our class under test as a server and allows for invoking it as if we were a HTTP Client. MockMvc will throw an exception should it not get a result it was expecting.
Have src/main/java/com/cognizant/studio/Application.java to look like the following;
package com.cognizant.studio;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication (1)
public class Application {
public static void main(String... args) {
SpringApplication.run(Application.class,args);(2)
}
}
-
Tells Spring that this is a Spring Application
-
Sets up the Spring Application Context
This can be tested by running the Spring Integration Test:
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationIT {
@Autowired
ApplicationContext applicationContext;
@Test
public void main() {
//test to see if the application context started up successfully
assertTrue(applicationContext.getBeanDefinitionCount() >= 0);
}
}