An overview of the

TDD(Test-driven Development) simply means writing tests and then writing code. This is the first rule, the unshakable rule, that writing code before writing tests is fake TDD.

Test-driven development can be divided into three cycles, red light, green light, and refactoring. It consists of the following steps:

  1. Write the test
  2. Run all tests
  3. Write the code
  4. Run all tests
  5. refactoring
  6. Run all tests

You write a test, you don’t pass it, you write the code, you run the test, you don’t pass the test, you pass the test, you turn green.

Tests fail, or the code needs to be refactored and all tests run again…

The next step is to demonstrate and illustrate the TDD process through a simple, RESTful Spring Boot Web project.

The functionality looks something like this: A simple element has two attributes: id and desc

Users to send a GET request HTTP interface http://localhost:8080/simples returns all simple json array element

1 Technical Tools

  1. JDK8+
  2. Spring 2.1 + Boot
  3. maven or Gradle
  4. JPA
  5. JUnit 5+
  6. Mockito
  7. Hamcrest

A common MVC architecture for RESTful request handling:

  1. The user accesses the HTTP URL
  2. Interface through the Controller layer
  3. The Controller layer calls the implementation of Service
  4. The Service interface accesses the database through the Repsoitory layer and ultimately returns data to the user

2 Build the Spring Boot project

Build a Spring Boot Maven project and add the required dependencies

The reference dependencies are as follows


    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.3.7. RELEASE</spring-boot.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
Copy the code

3 Start writing tests and code

1 Controller

First write a test to test the Controller layer. The Test code section creates a test class, SimpleControllerTest

Add two annotations @extendwith and @webmvctest.

Then add a MockMvc object to simulate the MVC request. In unit tests, each module should be tested independently. In the actual call chain, the Controller relies on the Service layer because it is currently being tested, and mocks the Service layer code, using an annotation

@MockBean

The entire code is as follows

@ExtendWith({SpringExtension.class})
@WebMvcTest
public class SimpleControllerTest {

    @Autowired
    MockMvc mockMvc;

    @MockBean
    private SimpleService simpleService;

}
Copy the code

SimpleService does not exist, compilation fails, red light, create it.

Thus creating a SimpleService as a Spring bean for the Service layer.

@Service
public class SimpleService {}Copy the code

Then write test code for the request /simples HTTP request

    @Test
    void testFindAllSimples(a) throws Exception {
        List<Simple> simpleList = new ArrayList<>();
        simpleList.add(new Simple(1L."one"));
        simpleList.add(new Simple(2L."two"));
        when(simpleService.findAll()).thenReturn(simpleList);

        mockMvc.perform(MockMvcRequestBuilders.get("/simples")
                .contentType(MediaType.APPLICATION_JSON)).andExpect(jsonPath("$", hasSize(2))).andDo(print());
    }
Copy the code

The when then structure comes from the Mockito framework. When represents the condition for execution, and then is used to perform validation. The operation here mocks the result of the simpleService. FindAll method. The follow perform method mocks the request for /simples.

Here you get an error, red light, and write an implementation of the Simple class.

@Entity
public class Simple {
    private Long id;
    private String desc;
    
    public Simple(String desc) {
        this.desc = desc; }}Copy the code

Because the simpleService. FindAll method is undefined, it still gives an error, a red light. Next, keep it simple and create a findAll method for SimpleService.

    public List<Simple> findAll(a) {
        return new ArrayList<>();
    }

Copy the code

With the compilation issues resolved, let’s run the test code.

An error,

Java. Lang. AssertionError: No value at JSON path "$"Copy the code

It’s still red because our mock perform doesn’t exist. Next, create a SimpleController class as RestController and write the interface to the /simples request.

@RestController
public class SimpleController {

    @Autowired
    private SimpleService simpleService;

    @GetMapping("/simples")
    public ResponseEntity<List<Simple>> getAllSimples() {
        return newResponseEntity<>(simpleService.findAll(), HttpStatus.OK); }}Copy the code

Run the test case again, all tests pass, green light.

2 Service

Let’s focus on code testing for the Service layer. The Test code area creates a SimpleServiceTest class. This class depends on the next layer of Repository and, again, creates a mock object for Repository.

@SpringBootTest
public class SimpleServiceTest {

    @MockBean
    private SimpleRepository simpleRepository;

}
Copy the code

Compiler error, red light, need to create a SimpleRepository.

@Repository
public interface SimpleRepository extends JpaRepository<Simple.Long> {}Copy the code

Above, create SimpleRepository JPA storage service as an object of the entity Simple class.

Write test code

    @Test
    void testFindAll(a) {
        Simple simple = new Simple("one");
        simpleRepository.save(simple);
        SimpleService simpleService = new SimpleService(simpleRepository);
        List<Simple> simples = simpleService.findAll();
        Simple entity = simples.get(simples.size() - 1);
        assertEquals(simple.getDesc(),entity.getDesc());
        assertEquals(simple.getId(),entity.getId());
    }
Copy the code

Continuing with the compilation error, SimpleService has no constructor. Add Repository and inject beans.

@Service
public class SimpleService {

    private SimpleRepository simpleRepository;



    public SimpleService(SimpleRepository simpleRepository) {
        this.simpleRepository = simpleRepository;
    }

    public List<Simple> findAll(a) {
        return newArrayList<>(); }}Copy the code

As an aside, Spring recommends injecting beans through constructors, making it easy to write testable code.

Running the test case will continue to generate errors, here because JPA Hibernate does not interact with the entity class object, requiring the addition of primary key annotations and the default constructor getter/setter to rewrite the entity class code.

@Entity
public class Simple {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String desc;

    public Simple(a) {}public Simple(String desc) {
        this.desc = desc;
    }

   // omit getter/setter...
   

}
Copy the code

The findAll method of SimpleService is modified and the findAll method of JPA Repository is called

    public List<Simple> findAll(a) {
        return simpleRepository.findAll();
    }
Copy the code

Now run the test case again and the test passes.

3 Repository

Previously, TDD has been passed to realize the code of Controller layer and Service layer. Theoretically, Repository realizes the interface of JPA. We have not done any code writing, so there is no need to test. To ensure database storage, inject the real JPA Respoitory instance into the Service object. Change @mockBean to @AutoWired.

@SpringBootTest
public class SimpleServiceTest {

    @Autowired
    private SimpleRepository simpleRepository;

    @Test
    void testFindAll(a) {
        Simple simple = new Simple("one");
        simpleRepository.save(simple);
        SimpleService simpleService = new SimpleService(simpleRepository);
        List<Simple> simpleEntities = simpleService.findAll();
        Simple entity = simpleEntities.get(simpleEntities.size() - 1); assertEquals(simple.getDesc(),entity.getDesc()); assertEquals(simple.getId(),entity.getId()); }}Copy the code

Create the H2 database configuration.

Classpath create schema. SQL and data. SQL, create the table and insert a bit of data.

#************H2 Begin****************
#The MySql statement location where the table is created
spring.datasource.schema=classpath:schema.sql
#The location of the MySql statement to insert data
spring.datasource.data=classpath:data.sql
#Disallow automatic creation of table structures based on entity, which are controlled by schema.sql
spring.jpa.hibernate.ddl-auto=none

spring.jpa.show-sql=true
Copy the code

schema.sql

DROP TABLE IF EXISTS simple;

CREATE TABLE `simple` (
 id  BIGINT(20) auto_increment,
 desc varchar(255));Copy the code

data.sql

INSERT INTO `simple`(`desc`) VALUES ('test1');
INSERT INTO `simple`(`desc`) VALUES ('test2');
Copy the code

Continue running the test cases, all of which pass, and the browser goes directly to localhost:8080/simples

Return the data inserted by data.sql

[{"id": 1."desc": "test1"
	},
	{
		"id": 2."desc": "test2"}]Copy the code

4 summarizes

The above is a complete TDD development process demonstration, each module test has independence, the current module, can mock the data of other modules. Regarding the structure of test cases, the AAA pattern is followed.

  1. Arrange: The first step in unit testing. You need to do the necessary test setup, such as creating the object of the target class, creating mock objects and initializing other variables if necessary, and so on
  2. Action: Calls the target method to be tested
  3. Assert: The final asynchrony of a unit test to check and verify that the results are consistent with the expected results.