Spring Boot uses unit testing blog address: tengJ.top /

preface

This time, I will introduce the integrated use of unit tests in Spring Boot. This article will introduce the following four points to basically meet daily needs

  • Service layer unit testing
  • Controller layer unit tests
  • New assertion assertThat is used
  • Rollback of unit tests

The body of the

Introducing unit tests in Spring Boot is simple, depending on the following:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-test</artifactId>
	<scope>test</scope>
</dependency>
Copy the code

In this example, the Spring Boot version is 1.5.9.RELEASE. After introducing spring-boot-starter-test, there are several libraries as follows: • JUnit — The de-facto standard for unit testing Java applications. • Spring Test & Spring Boot Test — Utilities and • AssertJ — A Fluent Assertion Library. • Hamcrest — A library of Matcher Objects (also known as constraints or predicates). • Mockito — A Java mocking framework Assertion Library for JSON. • JsonPath — XPath for JSON.

Service unit Testing

In Spring Boot, the unit test class is written in the SRC /test/ Java directory. You can manually create the specific test class. If it is IDEA, you can automatically create the test class through IDEA. Contributesto T(MAC) or Ctrl+Shift+T(Window) to create something like this:

The test classes are automatically generated as follows:

Then write the created test class, the specific code is as follows:

package com.dudu.service;
import com.dudu.domain.LearnResource;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import static org.hamcrest.CoreMatchers.*;

@RunWith(SpringRunner.class)
@SpringBootTest
public class LearnServiceTest {

    @Autowired
    private LearnService learnService;
    
    @Test
    public void getLearn(a){
        LearnResource learnResource=learnService.selectByKey(1001L);
        Assert.assertThat(learnResource.getAuthor(),is("Dudu MD Independent Blog")); }}Copy the code

Above is the simplest method to write unit tests. At the top, just use @runwith (sprinGrunner.class) and SpringBootTest. When you want to execute this method, right click on the corresponding method and choose Run.

I used the assertThat assertion in my test case, which IS described and recommended in the following sections.

Controller unit Test

The above tests are only for the Service layer, but sometimes you need to test the Controller layer (API), where MockMvc is used. You can test these interfaces without having to start the project.

MockMvc implements the simulation of Http requests and can directly use the form of the network to convert to the call of the Controller, which makes the test fast and independent of the network environment. Moreover, it provides a set of validation tools, which makes the verification of requests unified and convenient.

The Controller class:

package com.dudu.controller;

/** Created by tengj on 2017/3/13. */
@Controller
@RequestMapping("/learn")
public class LearnController  extends AbstractController{
    @Autowired
    private LearnService learnService;
    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @RequestMapping("")
    public String learn(Model model){
        model.addAttribute("ctx", getContextPath()+"/");
        return "learn-resource";
    }

    /** * query the tutorial list *@param page
     * @return* /
    @RequestMapping(value = "/queryLeanList",method = RequestMethod.POST)
    @ResponseBody
    public AjaxObject queryLearnList(Page<LeanQueryLeanListReq> page){
        List<LearnResource> learnList=learnService.queryLearnResouceList(page);
        PageInfo<LearnResource> pageInfo =new PageInfo<LearnResource>(learnList);
        return AjaxObject.ok().put("page", pageInfo);
    }
    
    /** * new tutorial *@param learn
     */
    @RequestMapping(value = "/add",method = RequestMethod.POST)
    @ResponseBody
    public AjaxObject addLearn(@RequestBody LearnResource learn){
        learnService.save(learn);
        return AjaxObject.ok();
    }

    /** * Modify tutorial *@param learn
     */
    @RequestMapping(value = "/update",method = RequestMethod.POST)
    @ResponseBody
    public AjaxObject updateLearn(@RequestBody LearnResource learn){
        learnService.updateNotNull(learn);
        return AjaxObject.ok();
    }

    /** * delete tutorial *@param ids
     */
    @RequestMapping(value="/delete",method = RequestMethod.POST)
    @ResponseBody
    public AjaxObject deleteLearn(@RequestBody Long[] ids){
        learnService.deleteBatch(ids);
        return AjaxObject.ok();
    }

    /** * get the tutorial *@param id
     */
    @RequestMapping(value="/resource/{id}",method = RequestMethod.GET)
    @ResponseBody
    public LearnResource qryLearn(@PathVariable(value = "id") Long id){
       LearnResource lean= learnService.selectByKey(id);
        returnlean; }}Copy the code

Here we also automatically create a Controller test class with the following code:

package com.dudu.controller;

import com.dudu.domain.User;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@RunWith(SpringRunner.class)
@SpringBootTest

public class LearnControllerTest {
    @Autowired
    private WebApplicationContext wac;

    private MockMvc mvc;
    private MockHttpSession session;


    @Before
    public void setupMockMvc(a){
        mvc = MockMvcBuilders.webAppContextSetup(wac).build(); // Initialize the MockMvc object
        session = new MockHttpSession();
        User user =new User("root"."root");
        session.setAttribute("user",user); // The interceptor will determine whether the user is logged in, so inject a user here
    }

    /** * New tutorial test case *@throws Exception
     */
    @Test
    public void addLearn(a) throws Exception{
        String json="{\"author\":\"HAHAHAA\",\"title\":\"Spring\",\"url\":\"http://tengj.top/\"}";
        mvc.perform(MockMvcRequestBuilders.post("/learn/add")
                    .accept(MediaType.APPLICATION_JSON_UTF8)
                    .content(json.getBytes()) // Pass json parameters
                    .session(session)
            )
           .andExpect(MockMvcResultMatchers.status().isOk())
           .andDo(MockMvcResultHandlers.print());
    }

    /** * get the tutorial test case *@throws Exception
     */
    @Test
    public void qryLearn(a) throws Exception {
        mvc.perform(MockMvcRequestBuilders.get("/learn/resource/1001")
                    .contentType(MediaType.APPLICATION_JSON_UTF8)
                    .accept(MediaType.APPLICATION_JSON_UTF8)
                    .session(session)
            )
           .andExpect(MockMvcResultMatchers.status().isOk())
           .andExpect(MockMvcResultMatchers.jsonPath("$.author").value("Dudu MD Independent Blog"))
           .andExpect(MockMvcResultMatchers.jsonPath("$.title").value("Spring Boot Dry Goods series"))
           .andDo(MockMvcResultHandlers.print());
    }

    /** * Modify the tutorial test case *@throws Exception
     */
    @Test
    public void updateLearn(a) throws Exception{
        String json="{\" the author \ ": \" test modified \ ", \ \ "id" : 1031, \ "title \" : \ "Spring Boot dry series \" and \ "url \" : \ "http://tengj.top/\"}";
        mvc.perform(MockMvcRequestBuilders.post("/learn/update")
                .accept(MediaType.APPLICATION_JSON_UTF8)
                .content(json.getBytes())// Pass json parameters
                .session(session)
        )
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcResultHandlers.print());
    }

    /** * Delete the tutorial test case *@throws Exception
     */
    @Test
    public void deleteLearn(a) throws Exception{
        String json="[1031].";
        mvc.perform(MockMvcRequestBuilders.post("/learn/delete")
                .accept(MediaType.APPLICATION_JSON_UTF8)
                .content(json.getBytes())// Pass json parameters.session(session) ) .andExpect(MockMvcResultMatchers.status().isOk()) .andDo(MockMvcResultHandlers.print()); }}Copy the code

The basic add, delete, change, and review test cases are implemented above. To use MockMvc, you need to build a MockMvc object using MockMvcBuilders, as shown below

@Before
public void setupMockMvc(a){
    mvc = MockMvcBuilders.webAppContextSetup(wac).build(); // Initialize the MockMvc object
    session = new MockHttpSession();
    User user =new User("root"."root");
    session.setAttribute("user",user); // The interceptor will determine whether the user is logged in, so inject a user here
}
Copy the code

Since the interceptor will determine whether to log in or not, I have injected a user here. You can also modify the interceptor to unvalidate user login and test it before enabling it.

Here’s an example of MockMvc’s simple approach

/** * get the tutorial test case *@throws Exception
 */
@Test
public void qryLearn(a) throws Exception {
    mvc.perform(MockMvcRequestBuilders.get("/learn/resource/1001")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .accept(MediaType.APPLICATION_JSON_UTF8)
                .session(session)
        )
       .andExpect(MockMvcResultMatchers.status().isOk())
       .andExpect(MockMvcResultMatchers.jsonPath("$.author").value("Dudu MD Independent Blog"))
       .andExpect(MockMvcResultMatchers.jsonPath("$.title").value("Spring Boot Dry Goods series"))
       .andDo(MockMvcResultHandlers.print());
}
Copy the code
  1. MockMvc. Perform executes a request
  2. MockMvcRequestBuilders. Get (“/user / 1 “) to construct a request, a Post request with. Post method
  3. ContentType (MediaType.APPLICATION_JSON_UTF8) represents the format of the data sent by the senderapplication/json; charset=UTF-8
  4. Accept (MediaType.APPLICATION_JSON_UTF8) represents the data type that the client wants to acceptapplication/json; charset=UTF-8
  5. Session (Session) Injects a session so that the interceptor can pass
  6. ResultActions. AndExpect adds assertions after execution
  7. ResultActions. AndExpect (MockMvcResultMatchers. The status (). IsOk () method to see if the state of the request the response code of 200 if not throw exceptions, not through test
  8. AndExpect (MockMvcResultMatchers. JsonPath (” $. The author “). The value (” doodle MD independent blog “)) here jsonPath is used to retrieve author field comparisonDudu MD independent blogNot fail the test
  9. ResultActions. Add a result andDo processor, said to do something, the results such as used here MockMvcResultHandlers. Print () outputs the response results information

This example tests as follows:

See mockMvc for more examples below this article

New assertion assertThat is used

JUnit 4.4 provides a new assertion syntax, assertThat, in conjunction with Hamcrest. A programmer can use assertThat as a single assertion statement, combined with the matching characters provided by Hamcrest, to express the whole idea of testing. The version we introduced is Junit4.12, so assertThat is supported.

The basic syntax for assertThat is as follows:

Listing 1 assertThat basic syntax

assertThat( [value], [matcher statement] );
Copy the code
  • Value is the variable value you want to test next;
  • The matcher statement is a declaration of the expected values of the preceding variables expressed in the Hamcrest card. If the values match the expected values expressed in the matcher statement, the test succeeds; otherwise, the test fails.

The advantages of assertThat

  • Advantage 1: Previously, JUnit provided a number of assertion statements, such as: AssertEquals, assertNotSame, assertFalse, assertTrue, assertNotNull, assertNull, etc. Now with JUnit 4.4, A single assertThat can replace all assertion statements and use only one assertion method in all unit tests, making it easier to write test cases, uniform code style, and easier to maintain test code.
  • Advantage 2: assertThat uses Hamcrest’s Matcher matching characters. Users can use matching criteria specified by matching characters to specify exactly what conditions they want to meet. It is very readable and more flexible to use. As shown in Listing 2:

Listing 2 shows a comparison between using Matcher and not using it

// Want to determine if a string s contains one of the substrings "developer" or "Works"
/ / JUnit 4.4 previous versions: assertTrue (s.i ndexOf (" developer ") > 1 | | s.i ndexOf (" Works ") > 1);
/ / JUnit 4.4:
assertThat(s, anyOf(containsString("developer"), containsString("Works"))); 
/ / clause anyOf the said any established a condition is met, similar to the logic or "| |", clause containsString said whether contain the parameters
// String. The article will discuss the matching characters in detail
Copy the code
  • Advantage 3: assertThat no longer uses the more difficult “subject” syntax pattern as assertEquals(e.g. AssertEquals (3, x);) Instead, assertThat uses a readable grammatical pattern similar to “subject-verb-object” (e.g. AssertThat (x,is(3));) , making the code more intuitive and easy to read.

  • Advantage 4: You can combine these Matcher cards and use them flexibly for more purposes. See Listing 3:

Listing 3 Matcher cards used in combination

// Combine not and equalTo to mean "not equal"
assertThat( something, not( equalTo( "developer")));// The combination of the not and containsString characters means "no substrings"
assertThat( something, not( containsString( "Works")));// The combination of anyOf and containsString means "contains any substring"
assertThat(something, anyOf(containsString("developer"), containsString("Works")));
Copy the code
  • Advantage 5: Error messages are more descriptive, readable, and descriptive. Previous versions of JUnit do not throw additional messages after errors, such as:
assertTrue( s.indexOf("developer") > -1 || s.indexOf("Works") > -1 );
Copy the code

If the assertion error, only sell useless error messages, such as junit) framework. An AssertionFailedError is generated: null. If you want to print out some useful prompts when errors occur, you must have programmers to manually write, such as:

assertTrue( "Expected a string containing 'developer' or 'Works'", 
    s.indexOf("developer") > -1 || s.indexOf("Works") > -1 );
Copy the code

Very inconvenient and requires extra code. JUnit 4.4 automatically provides readable descriptions by default, as shown in Listing 4: Listing 4 JUnit 4.4 provides readable descriptive error messages by default

String s = "hello world!"; 
assertThat( s, anyOf( containsString("developer"), containsString("Works")));// If an error occurs, the system will automatically throw the following message:
java.lang.AssertionError: 
Expected: (a string containing "developer" or a string containing "Works") 
got: "hello world!"
Copy the code

How do I use assertThat

JUnit 4.4 comes with some Hamcrest clause Matcher, but only a limited number, in the class org. Hamcrest. CoreMatchers defined, if you want to use them, Must import packages org. Hamcrest. CoreMatchers. *.

Listing 5 shows most of the examples of assertThat:

Character correlation card/**equalTo the equalTo character asserts that the testedValue being tested is equalTo the expectedValue. *equalTo can assert whether values are equal between strings and between objects, similar to the equals method of Object */
assertThat(testedValue, equalTo(expectedValue));
/**equalToIgnoringCase The matching card asserts that the string being tested, testedString *, is equal to the expectedString */ if case is ignored
assertThat(testedString, equalToIgnoringCase(expectedString));
/ * * equalToIgnoringWhiteSpace clause asserts that the string being measured testedString * in the head and tail of ignore any blank space under the condition of equal expectedString, * note: * / Spaces in the string cannot be neglected
assertThat(testedString, equalToIgnoringWhiteSpace(expectedString);
/**containsString Specifies the string testedString contains subString**/
assertThat(testedString, containsString(subString) );
/**endsWith asserts that the testedString testedString endsWith substring suffix */
assertThat(testedString, endsWith(suffix));
/**startsWith the test string testedString startsWith the substring prefix */assertThat(testedString, startsWith(prefix)); General card/**nullValue() asserts that the object under test is null*/
assertThat(object,nullValue());
/**notNullValue() asserts that the value of the tested object is not null*/
assertThat(object,notNullValue());
/**is asserts that the object under test is equal to the matching expression */
assertThat(testedString, is(equalTo(expectedValue)));
/** testedValue = expectedValue*/ is(equalTo(x)
assertThat(testedValue, is(expectedValue));
/**is (instanceOf(someclass.class)), * asserts that testedObject is an instanceOf Cheddar */
assertThat(testedObject, is(Cheddar.class));
The /**not card is the opposite of the is card, asserting that the tested object is not equal to the given object*/
assertThat(testedString, not(expectedString));
/**allOf card assertion meets all conditions, equivalent to "and" (&&) */
assertThat(testedNumber, allOf( greaterThan(8), lessThan(16)));/ * * anyOf clause one assertion accords with a condition, the equivalent of "or" (| |) * /
assertThat(testedNumber, anyOf( greaterThan(16), lessThan(8))); Numeric correlation card/**closeTo matching assertion for testedDouble at 20.0 Within A0.5 */
assertThat(testedDouble, closeTo( 20.0.0.5 ));
/**greaterThan indicates that the testedNumber is greaterThan 16.0*/
assertThat(testedNumber, greaterThan(16.0));
/** lessThan indicates that the testedNumber tested is lessThan 16.0*/
assertThat(testedNumber, lessThan (16.0));
/** greaterThanOrEqualTo specifies that the testedNumber tested is greaterThanOrEqualTo 16.0*/
assertThat(testedNumber, greaterThanOrEqualTo (16.0));
/** lessThanOrEqualTo specifies that the testedNumber tested is lessThanOrEqualTo 16.0*/
assertThat(testedNumber, lessThanOrEqualTo (16.0)); Set correlation card/**hasEntry Specifies that the Map object to be tested contains an Entry whose key value is "key" and whose element value is "value" */
assertThat(mapObject, hasEntry("key"."value"));/** the hasItem card indicates that the iterableObject under test contains the element Element and the test passes */
assertThat(iterableObject, hasItem (element));
/** hasKey specifies that the Map object to be tested contains the key "key" */
assertThat(mapObject, hasKey ("key"));
/** hasValue specifies that the Map object under test contains the element value value*/
assertThat(mapObject, hasValue(value));
Copy the code

Unit test rollback

To make sure you don’t make a Transactional Transactional when testing a unit, use the @Transactional annotation in the header of a method or class as follows:

@Test
@Transactional
public void add(a){
    LearnResource bean = new LearnResource();
    bean.setAuthor("Test rollback");
    bean.setTitle("Rollback use Case");
    bean.setUrl("http://tengj.top");
    learnService.save(bean);
}
Copy the code

This way the data will be rolled back after the test and will not cause garbage data. If you want to turn Rollback off, just add the @rollback (false) annotation. Rollback indicates that the transaction is rolled back after it is executed. You can pass in a value parameter. By default, the value is true and the value is false.

If you use a Mysql database and sometimes find that annotating @Transactional does not roll back, check to see if your default engine is InnoDB. If not, change to InnoDB.

MyISAM and InnoDB are two database storage engines commonly used by mysql at present. The main difference between MyISAM and InnoDB lies in performance and transaction control. Here is a brief introduction of the difference between the two and the conversion method:

  • MyISAM: MyISAM is the default database storage engine prior to MySQL5.5. MYISAM provides high-speed storage and retrieval, as well as full-text search capabilities, suitable for query-intensive applications such as data warehouses. But transactions are not supported and foreign keys are not supported. An important shortcoming of the MyISAM format is the inability to recover data after a table is corrupted.

  • InnoDB: InnoDB is the default database storage engine in MySQL5.5, but InnoDB has been acquired by Oracle and MySQL’s new storage engine Falcon will be introduced in MySQL6.0. InnoDB has transaction security with commit, rollback and crash recovery capabilities. But InnoDB writes less efficiently than the MyISAM storage engine and takes up more disk space to retain data and indexes. Nevertheless, InnoDB includes support for transactions and foreign keys, both of which are missing from MyISAM.

  • MyISAM is good for :(1) doing a lot of count calculations; (2) Insert is not frequent, query is very frequent; (3) No transaction.

  • InnoDB is suitable for :(1) high reliability requirements, or transactions; (2) table update and query are quite frequent, and the opportunity to lock the table is relatively large. (4) For servers with good performance, such as independent database servers, InnoDB engine is recommended for RDS of Ali Cloud.

Steps to modify the default engine

MySQL’s default storage engine

mysql> show variables like '%storage_engine%';
Copy the code

If you want to see what engine the user table uses (engine indicates the current storage engine of the table):

mysql> show create table user;
Copy the code

Alter table user as InnoDB storage engine (you can also use this command to change InnoDB to MyISAM) :

mysql> ALTER TABLE user ENGINE=INNODB;
Copy the code

If you want to change the storage engine of the entire database table, it is generally necessary to modify one table by one table, which is tedious. You can first export the database, get SQL, replace all MyISAM with INNODB, and then import the database. Restart mysql after the conversion is complete

service mysqld restart
Copy the code

conclusion

At this point, the Spring Boot integration unit testing is basically complete, and you can continue to explore the use of MockMvc and assertThat. The Swagger UI API documentation tool will be integrated in the future, which provides BOTH API documentation and test interface interface, which is quite easy to use.

For more Spring Boot dry products tutorials, go to: Spring Boot Dry Products Series Overview

reference

MockMVC explores new Junit 4.4 features

Spring Boot uses unit testing

Always feel that they write is not technology, but feelings, an article is the traces of their own way. Success relying on professional skills is the most replicable. I hope that my path can help you avoid detdetments, wipe away the dust of knowledge, and help you sort out the threads of knowledge. I hope that you and I can be on the top of technology in the future.