SpringBoot support for testing

Keywords: Programming Spring Junit Java JSON

Under the framework of micro-service, the whole system is cut into N independent micro-services to cooperate with each other, so there will be higher requirements for system availability. From large to small, it can be divided into three levels: unit testing for developer coding, interface debugging between micro-services and micro-services, and integration testing between micro-services and micro-services. The stability of the system can be effectively guaranteed by three-level rigorous testing.

  • As a developer, strict unit testing of code is the first step to ensure software quality. Spring Boot is an excellent collection of open source frameworks that are very friendly to test support. Spring Boot provides component Spring Boot Test that supports test. Spring Boot Test integrates seven powerful test frameworks that are popular in the industry:
    • JUnit: a unit testing framework of Java language
    • Spring Test: Provide integrated testing and tool support for Spring Boot applications
    • AssertJ: A Java testing framework that supports streaming assertions
    • Hamcrest: A Matcher Library
    • Mockito: A Java Mock Framework
    • JSONassert: An Assertion Library for JSON
    • JsonPath: JSON XPath Library

I. Quick Start

  • Add dependency
  • We create a spring-boot-test project to demonstrate the use of Spring Boot Test by adding spring-boot-starter-test dependencies to the project:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>
  • test method
  • First, I will demonstrate a simplest test, just testing the execution of a method:
public class HelloTest {
    @Test
    public void hello() {
        System.out.println("hello world");
    }
}
  • In Idea, click the helle() method name and select Run hello() to run. After execution, the console prints the following information:
hello world
  • Prove that the implementation of the method is successful.

  • Test service

  • In most cases, it is necessary to test the accuracy of a service in a project, which often requires a context after Spring Boot is launched. In this case, only two annotations are needed to support the situation. Let's create a HelloService service to demonstrate.

public interface HelloService {
    public void sayHello();
}
  • Create an implementation class for it:
@Service
public class HelloServieImpl implements HelloService {
    @Override
    public void sayHello() {
        System.out.println("hello service");
    }
}
  • In this implementation class, the sayHello() method outputs the string "hello service".
  • Spring Boot Test provides two annotations to enable you to get a post-boot context (Beans) in your tests. To test, just add @RunWith(SpringRunner.class) and @SpringBootTest annotations to the test class.
@RunWith(SpringRunner.class)
@SpringBootTest
public class HelloServiceTest {
    @Resource
    HelloService helloService;

    @Test
    public void  sayHelloTest(){
        helloService.sayHello();
    }
}
  • At the same time, Hello Service is injected into the test class. sayHello() method of Hello Service is called in the test method of SayHello Test. After executing the test method, it will be found that the startup information of Spring Boot is printed in the console. It shows that Spring Boot initializes the container before executing the test method and starts the output. The following information will be printed after the information:
hello service
  • Prove that the test service is successful. But this kind of test is a bit cumbersome, because the console prints too much, which requires us to distinguish carefully. Here is a more elegant solution. We can use Output Capture to judge whether the System outputs what we want. Add Output Capture and modify it as follows.
import static org.assertj.core.api.Assertions.assertThat;
import org.springframework.boot.test.rule.OutputCapture;

@RunWith(SpringRunner.class)
@SpringBootTest
public class HelloServiceTest {
    @Rule
    public OutputCapture outputCapture = new OutputCapture();
    @Resource
    HelloService helloService;
    @Test
    public void  sayHelloTest(){
        helloService.sayHello();
        assertThat(this.outputCapture.toString().contains("hello service")).isTrue();
    }
}
  • OutputCapture is a test class provided by Spring Book, which captures the output of System.out and System.err. We can use this feature to determine whether the output of the program is executed or not.

  • So when the output content is "hello service" test case execution is successful, if not, the execution will fail, and there is no need to pay attention to the console output content.

II. Web Testing

According to statistics, more than 90% of Java projects are Web projects. How to test the accuracy of the interface provided by Web projects becomes very important. In the past, we used to visit some specific addresses in the browser for testing, but it would be slightly more troublesome if some non-get requests were involved. Some friends will use the PostMan tool or write some Http Post requests to test, but it is not elegant and convenient after all.

In Spring Boot Test, MockMvc implements the simulation of Http requests. It can directly use the form of network and convert to the call of Controller. This can make the test faster and independent of the network environment. It also provides a set of verification tools, which can make the test faster and more independent of the network environment. Request verification is uniform and more convenient.

  • Next, to demonstrate, first add Web dependencies to the project:
<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
</dependency>
  • Create a HelloController method to export a hello to the outside world.
@RestController
public class HelloController {
    @RequestMapping(name="/hello")
    public String getHello() {
        return "hello web";
    }
}
  • Create Hello Web Test to test the getHello() method of the web interface we created above.
    • @Before Note that this means what needs to be done before the test case is executed. Here is the initialization of the test environment that needs to be established.
    • MockMvcRequestBuilders.post refers to support post requests, which can support various types of requests, such as get requests, put requests, patch requests, delete requests, etc.
    • andDo(print()), andDo(): Add ResultHandler result processor, print() prints out requests and corresponding content
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
@RunWith(SpringRunner.class)
@SpringBootTest
public class HelloWebTest {
    private MockMvc mockMvc;
    @Before
    public void setUp() throws Exception {
        mockMvc = MockMvcBuilders.standaloneSetup(new HelloController()).build();
    }
    @Test
    public void testHello() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.post("/hello")
                .accept(MediaType.APPLICATION_JSON_UTF8)).andDo(print());
    }
}
  • Console output:
MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /hello
       Parameters = {}
          Headers = {Accept=[application/json;charset=UTF-8]}
             Body = <no character encoding set>
    Session Attrs = {}

Handler:
             Type = com.neo.web.HelloController
           Method = public java.lang.String com.neo.web.HelloController.getUser()

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {Content-Type=[application/json;charset=UTF-8], Content-Length=[9]}
     Content type = application/json;charset=UTF-8
             Body = hello web
    Forwarded URL = null
   Redirected URL = null
          Cookies = []
  • From the information output above, we find that the whole request process is printed out, including the request header information, request parameters, return information and so on. According to the printed Body information, we can know that the getHello() method of HelloController has been successfully tested.

  • But sometimes we don't want to know the whole request process. We just need to verify whether the result returned is correct. We can make the following modifications:

@Test
public void testHello() throws Exception {
    mockMvc.perform(MockMvcRequestBuilders.post("/hello")
            .accept(MediaType.APPLICATION_JSON_UTF8))
//                .andDo(print())
             .andExpect(content().string(equalTo("hello web")));
}
  • If the interface return value is "hello web" test execution success, otherwise the test case execution fails. It also supports verifying whether the result set contains a specific string, which can be judged by using the containsString() method.
.andExpect(content().string(containsString("hello")));
  • Support direct conversion of result sets to string output:
String mvcResult= mockMvc.perform(MockMvcRequestBuilders.get("/messages")).andReturn().getResponse().getContentAsString();
System.out.println("Result === "+mvcResult);
  • Support for passing parameters on request:
@Test
public void testHelloMore() throws Exception {
    final MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
    params.add("id", "6");
    params.add("hello", "world");
    mockMvc.perform(
             MockMvcRequestBuilders.post("/hello")
            .params(params)
            .contentType(MediaType.APPLICATION_JSON_UTF8)
            .accept(MediaType.APPLICATION_JSON_UTF8))
            .andExpect(status().isOk())
            .andExpect(content().string(containsString("hello")));;
}
  • If the result is Json, you can use the following grammar to judge:
.andExpect(MockMvcResultMatchers.jsonPath("$.name").value("Pure smile"))
  • MockMvc provides a set of tool functions to perform the Assert judgment. This set of tools uses the chain call of functions to allow multiple test cases to be spliced together and multiple judgments to be made at the same time.

    • Performance builds a request and returns an instance of ResultActions that retrieves the returned content of the request.
    • params builds parameters when requests are made, and it also supports continuous addition of param(key,value).
    • contentType(MediaType.APPLICATION_JSON_UTF8) represents the data format sent by the sender.
    • accept(MediaType.APPLICATION_JSON_UTF8) represents the data type format that the client wants to accept.
    • mockMvc.perform() establishes a Web request.
    • andExpect(...) can be called many times after the call of perform(...) function to express the judgement of multiple conditions.
    • status().isOk() determines whether the request status returns 200.
    • The andReturn method returns the MvcResult object, which can obtain the returned view name, the returned Response status, the collection of interceptors for intercepting requests, and so on.

III. Use of JUnit

JUnit is a unit testing framework for the Java language. It is considered to be the most important third-party Java library developed so far. The advantage of JUnit is that the whole test process does not need the participation of people, does not need to analyze and judge whether the final test results are correct, and it is easy to run multiple tests at one time. The latest version of JUnit is Junit 5, which Spring Book integrates by default.

  • Following are common notes for Junit

    • @ Test: Mark a method as a test method
    • @ Before: Each test method is automatically invoked once before execution
    • @ After: Each test method is automatically called once after execution
    • @ BeforeClass: All test methods are executed once before they are executed, and the test class is loaded before it is instantiated, so modify it with static
    • @ AfterClass: All test methods are executed once and loaded before the test class is instantiated, so modify it with static
    • @ Ignore: Do not execute this test method for the time being
    • @ When a class is annotated with @RunWith or inherits a class annotated with @RunWith, JUnit calls the class it references to run the tests in that class instead of the developer building it inside junit. We use this feature in our development process.
  • Create a test class JUnit4Test class:

public class JUnit4Test {
    Calculation calculation = new Calculation();
    int result;//test result

    //Use @Test as a test method in JUnit 4
    @Test
    //The test method must be public void
    public void testAdd() {
        System.out.println("---testAdd Start testing---");

        //Each one is tested only once, because once the assertEquals test finds an error, it throws an exception and does not run subsequent code.
        result = calculation.add(1, 2);
        assertEquals(3, result);

        System.out.println("---testAdd End of normal operation---");
    }

    //Another test method
    //timeout indicates the number of milliseconds allowed for the test to execute, and expected indicates which thrown exceptions are ignored (the exception does not cause the test to fail)
    @Test(timeout = 1, expected = NullPointerException.class)
    public void testSub() {
        System.out.println("---testSub Start testing---");

        result = calculation.sub(3, 2);
        assertEquals(1, result);

        throw new NullPointerException();

        //System. out. println ("- - TesSubnormal Operation End -");
    }


    //Indicates that the [static method] will be executed before [all] test methods of this class are executed
    @BeforeClass
    public static void beforeAll() {
        System.out.println("||==BeforeClass==||");
        System.out.println("||==Usually, resources are loaded in this method==||");
    }

    //Indicates that the [static method] will be executed after [all] test methods of this class are executed
    @AfterClass
    public static void afterAll() {
        System.out.println("||==AfterClass==||");
        System.out.println("||==Usually in this way, resources are released.==||");
    }

    //This [member method] is executed before [each] test method is executed
    @Before
    public void beforeEvery() {
        System.out.println("|==Before==|");
    }

    //This [member method] is executed after [each] test method is executed
    @After
    public void afterEvery() {
        System.out.println("|==After==|");
    }

}
  • calculation is a self-defined calculator tool class. Specifically, you can refer to the sample project, execute the test class, and output:
||==BeforeClass==||
||== Usually, resources are loaded in this method==||
|==Before==|
TesAdd starts testing.
The normal operation of testAdd is over.
|==After==|
|==Before==|
TesSubStart Testing
|==After==|
||==AfterClass==||
||== Usually in this way, resources are released.==||
  • Comparing with the introduction above, you can clearly understand the use of each comment.

  • Assert uses

  • Assert is translated as "assertion" in Chinese. Readers who have used JUnit are familiar with the concept of assertion, which asserts that an actual operation value is the same as expected, otherwise an exception is thrown. Spring borrows the concept of method entry detection. The Assert class provided by Spring has many methods of asserting method entry according to rules, which can meet the requirements of most methods entry detection.

  • Spring Boot also provides assertive validation to help us validate the returned results of the method during testing.

@RunWith(SpringRunner.class)
@SpringBootTest
public class TestAssert {
    @Autowired
    private UserService userService;
    @Test
    public void TestAssert(){
        //Verify that the result is null
        Assert.assertNotNull(userService.getUser());
        //Verify that the results are equal
        Assert.assertEquals("i am neo!", userService.getUser());
        //Verification conditions are valid
        Assert.assertFalse(1+1>3);
        //Verify that objects are equal
        Assert.assertSame(userService,userService);
        int status=404;
        //Verify the result set, prompt
        Assert.assertFalse("Error, the correct return value is 200", status != 200);
        String[] expectedOutput = {"one", "two", "one"};
        String[] methodOutput = {"one", "one", "two"};
        //Verify that the array is the same
        Assert.assertArrayEquals(expectedOutput, methodOutput);
    }
}
  • From the examples above, we can find that using Assert can easily verify the test return results, avoid us writing a lot of if/else judgments, and make the code more elegant.

  • How to use assertThat

  • JUnit 4 learns from JMock and introduces Hamcrest matching mechanism, which makes programmers more readable and flexible when writing assert statements for unit tests. Hamcrest is a testing framework, which provides a set of universal matchers. By using the rules defined by these matchers flexibly, programmers can express their testing ideas more accurately and specify the testing conditions they want to set.

  • Assertion is one of the longest-used grammars in Junit. AssertThat is used to judge the text output by System. AssertThat is actually the latest grammatical sugar in JUnit 4. It can replace all previous assertions by using only one assertion sentence of assertThat and matching operators provided by Hamcrest. How to use it.

  • The basic syntax of assertThat is as follows:

    • Value is the value of the variable that you want to test next.
    • The matcher state is a declaration of the expected value of the previous variable expressed by the Hamcrest matcher. If the value value matches the expected value expressed by the matcher state, the test succeeds, otherwise the test fails.
assertThat( [value], [matcher statement] );
  • General matcher
// The allOf matcher indicates that if all the following conditions must be established before passing the test, it is equivalent to "and" (&)
assertThat( testedNumber, allOf( greaterThan(6), lessThan(33) ) );
// The anyOf matcher indicates that if only one of the following conditions holds, the test passes, which is equivalent to "or" (| |)
assertThat( testedNumber, anyOf( greaterThan(33), lessThan(6) ) );
// anything matchers indicate that no matter what the condition is, it will always be true
assertThat( testedNumber, anything() );
// The is matcher indicates that if the object to be tested before equals the object given later, the test passes.
assertThat( testedString, is( "keeppuresmile" ) );
// The not matcher is the opposite of the is matcher, indicating that if the object to be tested before is not equal to the object given later, the test passes.
assertThat( testedString, not( "keeppuresmile" ) );
  • String correlation matcher
// The containsString matcher indicates that if the test string testedString contains the substring "keep puresmile", the test passes.
assertThat( testedString, containsString( "keeppuresmile" ) );
// The endsWith matcher indicates that if the test string testedString ends with the substring "keep puresmile", the test passes.
assertThat( testedString, endsWith( "keeppuresmile" ) ); 
// The startsWith matcher indicates that if the test string testedString starts with the substring "keep puresmile", the test passes.
assertThat( testedString, startsWith( "keeppuresmile" ) ); 
// The equalTo matcher indicates that if the testedValue of the test is equal to expectedValue, the test passes, and equalTo can test between values.
//Is the equality between strings and objects equivalent to the equals method of Object
assertThat( testedValue, equalTo( expectedValue ) ); 
// The equalToIgnoringCase matcher indicates that if the tested string testedString ignores case, it is equal to
//"Keep puresmile" passed the test
assertThat( testedString, equalToIgnoringCase( "keeppuresmile" ) ); 
// The equalToIgnoring WhiteSpace matcher indicates that if the test string testedString ignores any spaces at the end and head, etc.
//In "keep puresmile", the test passes. Note: Spaces in strings cannot be ignored
assertThat( testedString, equalToIgnoringWhiteSpace( "keeppuresmile" ) );
  • NUMERICAL CORRELATION MATCHING
// The closeTo matcher indicates that if the tested floating-point number testedDouble is within the range of 10.0 + 0.3, the test passes.
assertThat( testedDouble, closeTo( 10.0, 0.3 ) );
// The greaterThan matcher indicates that if the tested value testedNumber is greater than 12.0, the test passes.
assertThat( testedNumber, greaterThan(12.0) );
// Less Than matchers indicate that if the tested value testedNumber is less than 12.0, the test passes.
assertThat( testedNumber, lessThan (12.0) );
// The greaterThanOrEqualTo matcher indicates that if the tested value testedNumber is greater than or equal to 12.0, the test passes.
assertThat( testedNumber, greaterThanOrEqualTo (12.0) );
// Less Than OrEqualTo matcher indicates that if the tested value testedNumber is less than or equal to 12.0, the test passes.
assertThat( testedNumber, lessThanOrEqualTo (12.0) );
  • collection correlation matcher
// hasEntry matcher indicates that if the Map object mapObject tested contains an Entry item whose key value is "key" and the corresponding element value is "value".
//Test pass
assertThat( mapObject, hasEntry( "key", "value" ) );
// hasItem matchers indicate that if iterableObject contains element "element" items, the test passes.
assertThat( iterableObject, hasItem ( "element" ) );
// The hasKey matcher indicates that if the Map object mapObject tested contains the key value, the test passes.
assertThat( mapObject, hasKey ( "key" ) );
// hasValue matcher indicates that if the Map object mapObject tested contains element value, the test passes.
assertThat( mapObject, hasValue ( "key" ) );

Specifically use the use of CalculationTest in the sample project.

  • Junt uses several suggestions:
    • The Test method must be decorated with @ Test
    • The test method must be modified with public void without any parameters.
    • Create a new source code directory to store our test code, separating the test code from the project business code
    • The package name of the test class should be the same as the package name of the tested class.
    • Each method in the test unit must be able to be tested independently, and there must be no dependency between test methods.
    • Test classes use Test as a suffix for class names (not required)
    • Test methods use test as prefix to method names (not required)

Four, summary

Spring Boot is an open source software with its own test components. Spring Boot Test contains seven powerful test tools, covering all aspects of testing. In practice, we only need to import Spring Boot Test to enable the project to have a variety of test functions. In order to effectively guarantee the quality of the project, three-tier test coverage should be strictly adopted under the micro-service architecture.

Posted by justbane on Mon, 23 Sep 2019 05:50:28 -0700