- What TDD actually is, is a cycle.
- You write a very simple test that fails. Then you write as little code as possible to make the test pass. You then write a slightly more complex test case that fails. Then you make it pass with as little code as possible. And around and around you go, in this cycle that should be complete in mere minutes (if not seconds).
- This cycle is known as the Red-> Green cycle.
- However, there is an extremely important step between the passes and the next failure. You must refactor your code where appropriate. You could make any test pass with enough if statements and hard-coding, but it would be useless code. Useful code is better. And when you refactor, you refactor without fear of breaking your existing functionality. You have no fear, because your full set of tests will let you know if anything breaks.
- This cycle is known as the Red-> Green-> Refactor cycle. This cycle is Test Driven Development.
- You will be fearless
- Code will be streamlined
- You will reduce debugging time.
- Your tests will become the most comprehensive set of documentation imaginable
- Your code will have better design
- Need not worry about code coverage
- Bugs in Tests
- Slower at the beginning
- All the members of the team need to do it
- Test need to be maintained when requirements changes
Mostly now a days we follow agile workflow model. So we will get requirements via UserStories. Here let's assume that we got one userstory with following endpoints.
Acceptance Criteria:
Expose below Rest URLS:
1./cars/{name} - Get Car details by name [GET]
2./cars/{name} - Throw CarNotFoundException
I'm using Intelij here where Spring Assistant plugin is used.
Select Spring Boot version and required Libraries.
Since we are going to use Junit 5 along with Spring boot Please include below dependencies too,
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-engine -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
Let's start from Controller. I'm going to take you to the tour where how step by step programming is happening with the help of Test Driven Development Approach. First we'll create CarControllerTest. Here we are going to create an end point for - /cars/{name}.
@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = CarController.class)
public class CarControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void getCar_Details() throws Exception{
mockMvc.perform(MockMvcRequestBuilders.get("/cars/Scala"))
.andExpect(status().isOk());
}
}
At line @WebMvcTest(controllers = CarController.class) code will give you an error saying that CarController class is not available. Hence we'll go to src/main folder and will create just CarController without Body.
public class CarController {
}
Now the compilation error is resolved. When we run CarControllerTest class now, we end-up with the failed message as below,
The reason for below error is that, because there is no rest endpoint with url /cars/Scala in CarController class. Let's create the endpoint in CarController class.
@RestController
@RequestMapping("/cars")
public class CarController {
@GetMapping("/{name}")
public ResponseEntity<Car> getCarDetails(@PathVariable String name) throws Exception {
return new ResponseEntity<>( HttpStatus.OK);
}
}
Once again we'll run the CarControllerTest class.
Yes it is passed now. Hurray!! guys we created endpoint successfully.
Now are focus is to return the Car details, for which we need Car model. Let's create Car model class under model package.
@Entity
@Table(name="CARS")
public class Car {
@Id
@GeneratedValue
private Long id;
@Column
private String name;
@Column
private String type;
public Car() {}
public Car(String name, String type) {
this.name = name;
this.type = type;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}
Now in CarController class lets add below codes,
@RestController
@RequestMapping("/cars")
public class CarController {
@GetMapping("/{name}")
public ResponseEntity<Car> getCarDetails(@PathVariable String name) throws Exception {
Car car = new Car();
return new ResponseEntity<>( car,HttpStatus.OK);
}
}
Now lets navigate to CarControllerTest class and add few more point to existing table as below,
@Test
public void getCar_Details() throws Exception{
mockMvc.perform(MockMvcRequestBuilders.get("/cars/Scala"))
.andExpect(status().isOk())
.andExpect(jsonPath("$").isMap())
.andExpect(jsonPath("name").value("Scala"))
.andExpect(jsonPath("type").value("Sadan"));
}
From above code, what are we expecting from test is that, when we pass /cars/Scala, then the response should contain Car object with Scala as name and Sadan as type.
Now we'll execute the test.
The reason for this that we are passing car object with null values in it.
Lets assume that from external class CarService, we will get the car details. Base on that assumption, in CarController lets change below line,
@Autowired
CarService carService;
@GetMapping("/{name}")
public ResponseEntity<Car> getCarDetails(@PathVariable String name) throws Exception {
Car car = carService.getCarDetails(name);
return new ResponseEntity<>( car,HttpStatus.OK);
}
Now we need to create CarService class with method getCarDetails without body.
@Service
public class CarService {
public Car getCarDetails(String name) {
return null;
}
}
As I said earlier Since we are going to focus only on controller we will mock any class which is external to CarController class.
Now in CarControllerTest, we are going to mock CarService class and give the definition for getCarDetails method present in it.
@MockBean
CarService carService;
@Test
public void getCar_Details() throws Exception{
given(carService.getCarDetails(Mockito.anyString())).willReturn(new Car("Scala","Sadan"));
mockMvc.perform(MockMvcRequestBuilders.get("/cars/Scala"))
.andExpect(status().isOk())
.andExpect(jsonPath("$").isMap())
.andExpect(jsonPath("name").value("Scala"))
.andExpect(jsonPath("type").value("Sadan"));
}
Here we are mocking CarService object with @MockBean annotation.
given(carService.getCarDetails(Mockito.anyString())).willReturn(new Car("Scala","Sadan"));
With above line we are defining the behaviour in such a way that, if we pass any String as name it should return new Car details. Now lets run the test once again. Yes it is passed.
Lets assume if no car details avaliable for the given name, what would happen. For this scenario we need to create CarNotFoundException class which will be throwed when no car details present for given name.
@ResponseStatus(code= HttpStatus.NOT_FOUND)
public class CarNotFoundException extends RuntimeException {
public CarNotFoundException() {}
}
Again from CarControllerTest we are going to have another test to validate this scenario.
@Test
public void Car_NotFoud_HttpStatus() throws Exception{
given(carService.getCarDetails(Mockito.anyString())).willThrow(new CarNotFoundException());
mockMvc.perform(MockMvcRequestBuilders.get("/cars/Scala"))
.andExpect(status().isNotFound());
}
If you run this test method it will work like charm. So far we completed two scenarios one with valid response and another with Exception throwed in Controller class level.
Now lets move to CarService Class. As we know so far we did not touch CarService class as part of CarControllerTest. Now Lets create CarServiceTest which is dedicated to CarService class.
Here we are going to use only Mockito related setup to ensure that how getCarDetails method is working. Here also we have two scenarios one with valid result from CarRepository and another with CarNotFoundException. Create CarRepository with findByName(name) interface first.
public interface CarRepository {
public Optional<Car> findByName(String name);
}
Now create CarServiceTest class,
public class CarServiceTest {
@Mock
CarRepository carRepository;
@InjectMocks
CarService carService;
@BeforeEach
public void setUp(){
MockitoAnnotations.initMocks(this);
}
@Test
public void getCarDetails() throws Exception{
given(carRepository.findByName("pulse")).willReturn(Optional.of(new Car("pulse", "hatchback")));
Car car = carService.getCarDetails("pulse");
assertNotNull(car);
assertEquals("pulse",car.getName());
assertEquals("hatchback",car.getType());
}
@Test
public void getCar_NotFound_Test(){
given(carRepository.findByName("pulse")).willThrow(new CarNotFoundException());
assertThrows(CarNotFoundException.class, ()-> carService.getCarDetails("pulse"));
}
}
As per above code we can see we did not bother about the logic behind CarRepository class. We are just mocking them by our expectations. we can run now. Yes both test methods are passed.
Now lets focus on CarRepository interface. We need to ensure that the CarRepository's method findByName should give us proper data fetched from database. Here we are going to use Embedded H2Database. Under src/main/resources folder add data.sql file just like below,
DROP TABLE IF EXISTS CARS;
CREATE TABLE CARS (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(250) NOT NULL,
type VARCHAR(250) NOT NULL
);
INSERT INTO CARS (id, name, type) VALUES ('1001','duster','hybrid');
INSERT INTO CARS (id, name, type) VALUES ('1002','micra','hatchback');
INSERT INTO CARS (id, name, type) VALUES ('1003','lodgy','suv');
What will happen here is that when we run @DataJpaTest annotated CarRepositoryTest class, these data will be stored in H2 Database untill the execution of test method is over.
Create CarRepositoryTest Class,
@ExtendWith(SpringExtension.class)
@DataJpaTest
class CarRepositoryTest {
@Autowired
private CarRepository carRepository;
@Test
public void testFindByName() {
Optional<Car> car = carRepository.findByName("duster");
assertTrue(car.isPresent());
}
@Test
public void testFindByName_Not_Found(){
Optional<Car> car = carRepository.findByName("pulse");
assertFalse(car.isPresent());
}
}
Lets run this,
Yes these cases passed. If you see the highlighted part, the query is executed to fetch the data from database.
Now lets focus on Cache test in our application. Lets go and add @EnableCaching to Application class,
@SpringBootApplication
@EnableCaching
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Now go to CarService class and annotate getCarDetails method with @Cacheable("cars").
Lets create CacheTest class. Just be clear that since we are going to verify cache, we need to use @SpringBootTest in this class,
@SpringBootTest
public class CacheTest {
@MockBean
CarRepository carRepository;
@Autowired
CarService carService;
@Test
void cacheTest() {
given(carRepository.findByName("pulse")).willReturn(Optional.of(new Car("pulse", "hatchback")));
Car car = carService.getCarDetails("pulse");
assertNotNull(car);
carService.getCarDetails("pulse");
Mockito.verify(carRepository,Mockito.times(1)).findByName("pulse");
}
}
Here with the help of Mockito's verify method we are ensuring that carRepository's findByName method is called only once,though we called carService.getCarDetails() twice.
Now let create IntegrationTest class to ensure entire flow is working fine.
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT)
public class IntegrationTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Autowired
CarService carService;
HttpHeaders headers = new HttpHeaders();
@Test
public void getCarDetails() throws Exception {
HttpEntity<String> entity = new HttpEntity<String>(null,headers);
ResponseEntity<Car> response = restTemplate.exchange(
"http://localhost:"+port+"/cars/duster", HttpMethod.GET, entity, Car.class);
assertEquals(HttpStatus.OK,response.getStatusCode());
assertEquals("hybrid",response.getBody().getType());
}
}
If we run all Test at once we may get following out put from Intelij,
So finally we came to end of the session. That all about Test Driven Development approach towards creation of Spring Boot application.