Test Driven Development is the process in which test cases are written before the code that validates those cases. It depends on the repetition of a very short development cycle. Test Driven Development is a technique in which automated Unit tests are used to drive the design and free decoupling of dependencies. In this article via a sample project let us see the Test Driven Development with JUnit5 and Mockito with integration and functional test as a maven project.
Advantages of JUnit5:
- It supports code written from Java 8 onwards making tests more powerful and maintainable. For the sample project, Java 11 with Maven 3.5.2 or higher is taken.
- Display name feature is there. It can be organized hierarchically.
- It can use more than one extension at a time.
Example Project
Project Structure:
As this is the maven project, let us see the necessary dependencies via
pom.xml
<? xml version = "1.0" encoding = "UTF-8" ?>
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
< modelVersion >4.0.0</ modelVersion >
< groupId >gfg.springframework</ groupId >
< artifactId >sampletest-junit5-mockito</ artifactId >
< version >1.0-SNAPSHOT</ version >
< name >sampletest-junit5-mockito</ name >
< description >Testing Java with JUnit 5</ description >
< properties >
< project.build.sourceEncoding >UTF-8</ project.build.sourceEncoding >
< project.reporting.outputEncoding >UTF-8</ project.reporting.outputEncoding >
< java.version >11</ java.version >
< maven.compiler.source >${java.version}</ maven.compiler.source >
< maven.compiler.target >${java.version}</ maven.compiler.target >
< junit-platform.version >5.3.1</ junit-platform.version >
</ properties >
< dependencies >
< dependency >
< groupId >javax.validation</ groupId >
< artifactId >validation-api</ artifactId >
< version >2.0.1.Final</ version >
</ dependency >
< dependency >
< groupId >org.apache.commons</ groupId >
< artifactId >commons-lang3</ artifactId >
< version >3.8.1</ version >
</ dependency >
< dependency >
< groupId >org.junit.jupiter</ groupId >
< artifactId >junit-jupiter-api</ artifactId >
< version >${junit-platform.version}</ version >
< scope >test</ scope >
</ dependency >
< dependency >
< groupId >org.junit.jupiter</ groupId >
< artifactId >junit-jupiter-params</ artifactId >
< version >${junit-platform.version}</ version >
</ dependency >
< dependency >
< groupId >org.junit.jupiter</ groupId >
< artifactId >junit-jupiter-engine</ artifactId >
< version >${junit-platform.version}</ version >
< scope >test</ scope >
</ dependency >
< dependency >
< groupId >org.assertj</ groupId >
< artifactId >assertj-core</ artifactId >
< version >3.11.1</ version >
< scope >test</ scope >
</ dependency >
< dependency >
< groupId >org.hamcrest</ groupId >
< artifactId >hamcrest-library</ artifactId >
< version >1.3</ version >
< scope >test</ scope >
</ dependency >
</ dependencies >
< build >
< plugins >
< plugin >
< groupId >org.apache.maven.plugins</ groupId >
< artifactId >maven-compiler-plugin</ artifactId >
< version >3.8.0</ version >
</ plugin >
< plugin >
< groupId >org.apache.maven.plugins</ groupId >
< artifactId >maven-surefire-plugin</ artifactId >
< version >2.22.0</ version >
< configuration >
< argLine >
--illegal-access=permit
</ argLine >
</ configuration >
</ plugin >
< plugin >
< groupId >org.apache.maven.plugins</ groupId >
< artifactId >maven-failsafe-plugin</ artifactId >
< version >2.22.0</ version >
< configuration >
< argLine >
--illegal-access=permit
</ argLine >
</ configuration >
< executions >
< execution >
< goals >
< goal >integration-test</ goal >
< goal >verify</ goal >
</ goals >
</ execution >
</ executions >
</ plugin >
< plugin >
< groupId >org.apache.maven.plugins</ groupId >
< artifactId >maven-site-plugin</ artifactId >
< version >3.7.1</ version >
</ plugin >
</ plugins >
</ build >
< reporting >
< plugins >
< plugin >
< groupId >org.apache.maven.plugins</ groupId >
< artifactId >maven-surefire-report-plugin</ artifactId >
< version >2.22.0</ version >
</ plugin >
</ plugins >
</ reporting >
</ project >
|
Let us see the very very important files of the project. Let’s start with the Model class
BaseEntity.java
import java.io.Serializable;
public class BaseEntity implements Serializable {
private Long id;
public boolean isNew() {
return this .id == null ;
}
public BaseEntity() {
}
public BaseEntity(Long id) {
this .id = id;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this .id = id;
}
} |
Geek.java
public class Geek extends BaseEntity {
public Geek(Long id, String firstName, String lastName) {
super (id);
this .firstName = firstName;
this .lastName = lastName;
}
private String firstName;
private String lastName;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this .firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this .lastName = lastName;
}
} |
Author.java
public class Author extends Geek {
private String address;
private String city;
private String telephone;
public Author(Long id, String firstName, String lastName) {
super (id, firstName, lastName);
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this .address = address;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this .city = city;
}
public String getTelephone() {
return telephone;
}
public void setTelephone(String telephone) {
this .telephone = telephone;
}
} |
AuthorType.java
public enum AuthorType {
FREELANCING, COMPANY
} |
AuthorController.java
import javax.validation.Valid;
import gfg.springframework.model.Author;
import gfg.springframework.services.AuthorService;
import gfg.springframework.spring.BindingResult;
import gfg.springframework.spring.Model;
import gfg.springframework.spring.ModelAndView;
import gfg.springframework.spring.WebDataBinder;
import java.util.List;
public class AuthorController {
private static final String VIEWS_AUTHOR_CREATE_OR_UPDATE_FORM = "authors/createOrUpdateAuthorForm" ;
private final AuthorService authorService;
public AuthorController(AuthorService authorService) {
this .authorService = authorService;
}
public void setAllowedFields(WebDataBinder dataBinder) {
dataBinder.setDisallowedFields( "id" );
}
public String findAuthors(Model model){
model.addAttribute( "author" , new Author( null , null , null ));
return "authors/findAuthors" ;
}
public String processFindForm(Author author, BindingResult result, Model model){
// allow parameterless GET request for
// authors to return all records
if (author.getLastName() == null ) {
// empty string signifies
// broadest possible search
author.setLastName( "" );
}
// find authors by last name
List<Author> results = authorService.findAllByLastNameLike( "%" + author.getLastName() + "%" );
if (results.isEmpty()) {
// no authors found
result.rejectValue( "lastName" , "notFound" , "not found" );
return "authors/findAuthors" ;
} else if (results.size() == 1 ) {
// 1 author found
author = results.get( 0 );
return "redirect:/authors/" + author.getId();
} else {
// multiple authors found
model.addAttribute( "selections" , results);
return "authors/authorsList" ;
}
}
public ModelAndView showAuthor(Long authorId) {
ModelAndView mav = new ModelAndView( "authors/authorDetails" );
mav.addObject(authorService.findById(authorId));
return mav;
}
public String initCreationForm(Model model) {
model.addAttribute( "author" , new Author( null , null , null ));
return VIEWS_AUTHOR_CREATE_OR_UPDATE_FORM;
}
public String processCreationForm( @Valid Author author, BindingResult result) {
if (result.hasErrors()) {
return VIEWS_AUTHOR_CREATE_OR_UPDATE_FORM;
} else {
Author savedAuthor = authorService.save(author);
return "redirect:/authors/" + savedAuthor.getId();
}
}
public String initUpdateAuthorForm(Long authorId, Model model) {
model.addAttribute(authorService.findById(authorId));
return VIEWS_AUTHOR_CREATE_OR_UPDATE_FORM;
}
public String processUpdateAuthorForm( @Valid Author author, BindingResult result, Long authorId) {
if (result.hasErrors()) {
return VIEWS_AUTHOR_CREATE_OR_UPDATE_FORM;
} else {
author.setId(authorId);
Author savedAuthor = authorService.save(author);
return "redirect:/authors/" + savedAuthor.getId();
}
}
} |
AuthorRepository.java
import java.util.List;
import gfg.springframework.model.Author;
public interface AuthorRepository extends CrudRepository<Author, Long> {
Author findByLastName(String lastName);
List<Author> findAllByLastNameLike(String lastName);
} |
AuthorService.java
import java.util.List;
import gfg.springframework.model.Author;
public interface AuthorService extends CrudService<Author, Long> {
Author findByLastName(String lastName);
List<Author> findAllByLastNameLike(String lastName);
}
|
AuthorMapService.java
import java.util.List;
import java.util.Set;
import gfg.springframework.model.Author;
import gfg.springframework.services.AuthorService;
public class AuthorMapService extends AbstractMapService<Author, Long> implements AuthorService {
@Override
public Set<Author> findAll() {
return super .findAll();
}
@Override
public Author findById(Long id) {
return super .findById(id);
}
@Override
public Author save(Author object) {
if (object != null ){
return super .save(object);
} else {
return null ;
}
}
@Override
public void delete(Author object) {
super .delete(object);
}
@Override
public void deleteById(Long id) {
super .deleteById(id);
}
@Override
public Author findByLastName(String lastName) {
return this .findAll()
.stream()
.filter(author -> author.getLastName().equalsIgnoreCase(lastName))
.findFirst()
.orElse( null );
}
} |
Let us see the test files now
ControllerTests.java
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.TestInstance;
@TestInstance (TestInstance.Lifecycle.PER_CLASS)
@Tag ( "controllers" )
public interface ControllerTests {
@BeforeAll
default void beforeAll(){
System.out.println( "beforeAll-Initialization can be done here" );
}
} |
ModelRepeatedTests.java
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.RepetitionInfo;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.TestInfo;
@Tag ( "repeated" )
public interface ModelRepeatedTests {
@BeforeEach
default void beforeEachConsoleOutputer(TestInfo testInfo, RepetitionInfo repetitionInfo){
System.out.println( "Running Test - " + testInfo.getDisplayName() + " - "
+ repetitionInfo.getCurrentRepetition() + " | " + repetitionInfo.getTotalRepetitions());
}
} |
ModelTests.java
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.TestInfo;
@Tag ( "model" )
public interface ModelTests {
@BeforeEach
default void beforeEachConsoleOutputer(TestInfo testInfo){
System.out.println( "Running Test - " + testInfo.getDisplayName());
}
} |
AuthorTest.java
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.ValueSource;
import gfg.springframework.model.Author;
import gfg.springframework.model.AuthorType;
import gfg.springframework.test.ModelTests;
class AuthorTest implements ModelTests {
@Test
void assertionsTest() {
Author author = new Author(1l, "Rachel" , "Green" );
author.setCity( "Seatle" );
author.setTelephone( "1002003001" );
assertAll( "Properties Test" ,
() -> assertAll( "Geek Properties" ,
() -> assertEquals( "Rachel" , author.getFirstName(), "First Name Did not Match" ),
() -> assertEquals( "Green" , author.getLastName())),
() -> assertAll( "Author Properties" ,
() -> assertEquals( "Seatle" , author.getCity(), "City Did Not Match" ),
() -> assertEquals( "1002003001" , author.getTelephone())
));
assertThat(author.getCity(), is( "Seatle" ));
}
@DisplayName ( "Value Source Test" )
@ParameterizedTest (name = "{displayName} - [{index}] {arguments}" )
@ValueSource (strings = { "Spring" , "Framework" , "GFG" })
void valueSourceTest(String val) {
System.out.println(val);
}
@DisplayName ( "Enum Source Test" )
@ParameterizedTest (name = "{displayName} - [{index}] {arguments}" )
@EnumSource (AuthorType. class )
void enumTest(AuthorType authorType) {
System.out.println(authorType);
}
} |
Here we can see that can include more than one annotations
- @DisplayName: Purpose of the test and categorization can be done easily
- @Parameterized Tests – They are built in and adopt the best features from JUnit4Parameterized and JUnitParams of Junit4
It helps to go with @ValueSource. @EmptySource and @NullSource represent a single parameter. On running the above code, we can able to get the below output
GeekTest.java
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
import gfg.springframework.model.Geek;
import gfg.springframework.test.ModelTests;
class GeekTest implements ModelTests {
@Test
void groupedAssertions() {
// given
Geek person = new Geek(1l, "Ross" , "Geller" );
// then
assertAll( "Test Props Set" ,
() -> assertEquals(person.getFirstName(), "Ross" ),
() -> assertEquals(person.getLastName(), "Geller" ));
}
@Test
void groupedAssertionMsgs() {
// given
Geek person = new Geek(1l, "Chandler" , "Bing" );
// then
assertAll( "Test Props Set" ,
() -> assertEquals(person.getFirstName(), "Ross" , "Input First Name is wrong" ),
() -> assertEquals(person.getLastName(), "Geller" , "Input Last Name is wrong" ));
}
} |
On running the above, the first test is ok and second one fails as expected and the actual one does not match
AuthorMapServiceTest.java
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import gfg.springframework.model.Author;
import gfg.springframework.services.map.AuthorMapService;
@DisplayName ( "Author Map Service Test - " )
class AuthorMapServiceTest {
AuthorMapService authorMapService;
@BeforeEach
void setUp() {
authorMapService = new AuthorMapService();
}
@DisplayName ( "Verifying that there are Zero Authors" )
@Test
void authorsAreZero() {
int authorCount = authorMapService.findAll().size();
assertThat(authorCount).isZero();
}
@DisplayName ( "Saving Authors Tests - " )
@Nested
class SaveAuthorsTests {
@BeforeEach
void setUp() {
authorMapService.save( new Author(1L, "Before" , "Each" ));
}
@DisplayName ( "Saving Author" )
@Test
void saveAuthor() {
Author author = new Author(2L, "Joe" , "Tribbiani" );
Author savedAuthor = authorMapService.save(author);
assertThat(savedAuthor).isNotNull();
}
@DisplayName ( "Save Authors Tests - " )
@Nested
class FindAuthorsTests {
@DisplayName ( "Find Author" )
@Test
void findAuthor() {
Author foundAuthor = authorMapService.findById(1L);
assertThat(foundAuthor).isNotNull();
}
@DisplayName ( "Find Author Not Found" )
@Test
void findAuthorNotFound() {
Author foundAuthor = authorMapService.findById(2L);
assertThat(foundAuthor).isNull();
}
}
}
@DisplayName ( "Verify Still Zero Authors" )
@Test
void authorsAreStillZero() {
int authorCount = authorMapService.findAll().size();
assertThat(authorCount).isZero();
}
} |