I wrote the test code of MVC related processing and login related processing in Spring Boot through trial and error, so I will summarize it as a memorandum.
If you know how to write better, I would be grateful if you could kindly teach me (╹◡╹)
The source code is available on GitHub.
In this article, we will focus on how to write test code in Spring Boot, so if you meet the following, it will be painful.
With the above assumptions, reading this article will (probably) enable you to:
In addition, this time, the screen display and the test of the JS part etc. are excluded. In the future, I'll cover that area as well, but suddenly trying to test everything would be too complicated, so I'll start small. Step up is important.
A simple diagram of the above coverage is as follows.
I will touch on the area from when the request is thrown to when View is requested to display it. Therefore, View is basically good night this time.
For details, please see pom.xml on GitHub.
Spring-Boot(2.1.8)
Starter settings
Test related pom.xml
pom.xml(Excerpt)
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.dbunit/dbunit -->
<dependency>
<groupId>org.dbunit</groupId>
<artifactId>dbunit</artifactId>
<version>2.5.1</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.springtestdbunit/spring-test-dbunit -->
<dependency>
<groupId>com.github.springtestdbunit</groupId>
<artifactId>spring-test-dbunit</artifactId>
<version>1.3.0</version>
<scope>test</scope>
</dependency>
Now, let's see how to actually write test code from here. However, Spring Boot test code is written by combining some knowledge, so if you try to cover everything at once, the difficulty level will jump up.
Therefore, I would like to divide it into the following four steps.
Level1. Test Hello World
Level2. Test database operations
Level3. Test POST request
(Level 4. Test apps that require login)
Regarding level 4, it is necessary to mobilize all the knowledge of the test code taken up in this article, and the volume will be large, so I will divide it into another article.
The coverage of Levels 1 to 3 covered in this article is roughly illustrated as follows.
Let's start with Hello World.
This is the familiar Hello World. Let's take a look at some common processes, such as when a request is thrown to "/ hello / init" below, "hello" is returned as the view name.
HelloController.java
@Controller
@RequestMapping("/hello")
public class HelloController {
@RequestMapping("/init")
private String init() {
return "hello";
}
}
It's simple to do, but it requires some new knowledge to write test code, so it's a very important part of getting started with Spring Boot test code.
First of all, the code itself is not very long, so to get the big picture, write the actual test code below.
HelloControllerTest.java
@AutoConfigureMockMvc
@SpringBootTest(classes = DbMvcTestApplication.class)
public class HelloControllerTest {
//mockMvc Mock object for handling Http request and response without deploying to Tomcat server
@Autowired
private MockMvc mockMvc;
//Specify view in get request and judge success / failure of request by http status
@Test
void init processing runs and returns 200() throws Exception {
// andDo(print())Show request response with
this.mockMvc.perform(get("/hello/init")).andDo(print())
.andExpect(status().isOk());
}
Suddenly, when I was writing an app, I found a lot of things that I wasn't familiar with. The processing of this test code is roughly divided into "two" blocks, so let's take a closer look at each.
The annotations given to the class have important information to get an overall picture of the test code. I don't usually use it, but if you get a good overview, the distance to the test code will be shortened.
It is an annotation to use something called MockMvc
. So who is MockMvc?
This is to separate the physical "server" from the created "web application". It doesn't come out very well, so let's look at what makes us happy by disconnecting it.
For personal development, mock the server part to reduce the execution time of the test code, and for team development, mock the part that you are not involved in and write the test code after limiting the range of influence. I think it's better to proceed.
The following is a reference material.
Japanese material of MockMvc Official Web Layer Test
This is an annotation that seems to be very important. It's actually a very important annotation. In the spring boot unit test, annotations that almost always appear and have many functions, so I would like to take a step-by-step look at the functions.
The following two functions are important here.
First, let's talk about ʻExtendWith annotation``. The RunWith annotation was often used in the explanation of the Spring Boot test, but the RunWith annotation
is an annotation for Junit4, and the ExtendWith annotation is an annotation for Junit5.
It is used for general-purpose implementation of test pre-processing and post-processing. Then, SpringExtension is passed to the "Extension" class where the implementation of general-purpose processing, which is the value property, is written. Since it is an advanced story, I will omit it this time, but this Extension class plays an important part such as instantiation of ApplicationContext which has the role of DI container. See Official for details.
Well, as I wrote for a long time, using a DI container in Spring Boot is no longer a matter of course, and writing the ExtendWith annotation every time is troublesome. As you can see from the Documentation, it is natural to use it. If it is, it is okay to include it, so only the SpringBootTest annotation is OK.
By creating annotations that include multiple functions in this way, it is possible to simplify the description of the premise of the test code, but this time we will emphasize clarity, so for integration Annotations are omitted.
Next, let's take a look at the ApplicationContext settings. ApplicationContext is often used as a word, but I think it's okay if you think of it as a "DI container".
SpringBootApplication annotation
in the classes property, the setting class with Configuration annotation and the target class of ComponentScan are automatically set. It will be registered in the DI container.
ReferenceIt's not possible to solve everything by simply passing the main class, but I'd like to touch on that in the area of testing at the Dao layer, which does not require HTTP request / response.
The explanation has become long just by setting, but once you understand it, it is an important part that can be used when writing other test code, so it may be good to sit down and study. .. (I understood that I didn't understand DI at all, so it was a good opportunity to review Spring)
I finally got to the actual test code. It's simpler and more fun than the detailed settings so far. Now that the intervals are open, let's take a look at the part related to the test code again.
HelloControllerTest.java(Excerpt)
//Specify view in get request and judge success / failure of request by http status
@Test
void init processing runs and returns 200() throws Exception {
// andDo(print())Show request response with
this.mockMvc.perform(get("/hello/init")).andDo(print())
.andExpect(status().isOk());
}
The above test code performs two processes, "execution of request" and "verification of response".
Both processes are performed based on the instance of "MockMvc", and are basically described in one statement as described above. By putting them together in one sentence, it is possible to unravel what the test is doing in the form of an English sentence. Let's see how it is actually written.
To summarize the above, the English text will be as follows. (I'm not very good at writing, so please feel it in the atmosphere ƪ (˘⌣˘) ʃ)
Perform get request to [/hello/init] and print the result, and I expect that status is 200 OK.
Since the programming language is written in English, there is of course the advantage that native English speakers can read and write test code in a structure similar to the English sentences they are accustomed to. (There seems to be no way to escape from English, so it may not be painful if you at least bite into reading and listening.)
It's a little off topic, but by describing the test method and expected results in an easy-to-read format, important information for understanding the system specifications and the actual source code can be easily obtained from the test code. Become. On the flip side, test code that doesn't meet the spec will miss bugs and won't help you understand the spec, so it's bad code.
From such a simple code stage, it may be good to get into the habit of writing test code while always being aware of what should be realized as a function in the implemented process
.
After a lot of digressions, the request response when Hello World is verified with the test code is as follows.
You should be able to confirm that the contents described above are satisfied.
Finally, I can confirm that Hello World is working properly. I did it.
Since there is a limit to what can be verified with the HelloWorld test code alone, I would like to take a look at Model verification as another practical thing at Level 1.
Model here refers to "Java object referenced by View" and is often written in Model.addAttribute
etc.
It's very common that when I thought that I packed the value in the Model nicely, it wasn't actually entered, so I didn't have to start the server and access the screen with a click ... It would be very useful if we could verify that it was working.
Below, we will see how to actually verify the contents of the Model with test code. The content is easier to understand than the ones I've touched on so far, so I hope you'll master how to use it.
First, let's look at the code to be tested. That said, it's just a slight extension of HelloController, and it's not that difficult throughout, so I'll put it all at once here.
HelloController.java
@Controller
@RequestMapping("/hello")
public class HelloController {
@RequestMapping("/init")
private String init(Model model) {
//User list First, manually generate
List<User> userList = new ArrayList<User>();
User user = new User();
user.setUserId(0L);
user.setUserName("test0");
User user2 = new User();
user2.setUserId(1L);
user2.setUserName("test1");
userList.add(user);
userList.add(user2);
//Set a list of users on the form and add it to the model to lay the groundwork for verifying that it was successfully added to the model.
DbForm form = new DbForm();
form.setUserList(userList);
model.addAttribute("message", "hello!");// 1
model.addAttribute("user", user);// 2
model.addAttribute("dbForm", form);// 3
return "hello";
}
}
In the following, we will follow each pattern for model.addAttribute
.
Let's start with a simple example.
In model.addAttribute ("message "," hello! ");
, The model simply stores "message" as the key and "hello" as the value.
The test code to verify this is as follows.
HelloControllerTest.java(Excerpt)
@Test
hello is passed to the model message in void init processing() throws Exception {
this.mockMvc.perform(get("/hello/init"))
.andExpect(model().attribute("message", "hello!"));
}
The test code is also very simple, and as you can see from the part ʻandExpect (model (). Attribute ("message", "hello!")) `, It is almost the same as stuffing into the actual Model. You can also write test code.
If you actually look at the result, you can see that the Model part is correctly packed with values.
If it is a simple object, the property is only one level, so it can be written simply. However, if you take a closer look at the results, you will find that the value value is written in a dubious string. It represents the instance of the object itself. Of course, there are times when you want to verify that an object isn't null, but most of the time you want to know if a particular property in the object has the expected value.
Below we'll look at validating a Model packed with these nested properties.
Now, verifying nested objects makes the test code a bit more complicated, but it's much simpler than interpreting annotations, so let's explore each one.
Here, regarding the processing of model.addAttribute ("user ", user);
, verify whether the property "userName" of the "user" instance is as expected (here, the value "test0"). To go.
Anyway, let's start by looking at the actual test code.
HelloControllerTest.java(Excerpt)
@Test
User Entity is stored in the model by void init processing() throws Exception {
this.mockMvc.perform(get("/hello/init"))
.andExpect(model()
.attribute("user", hasProperty(
"userName", is("test0")
)
)
);
}
Suddenly the structure changed drastically. This is not the Spring Boot test code, but rather the processing by the framework that handles the so-called "Matcher" that verifies the validity of the test called "Hamcrest".
The method hasProperty
for validating the properties of an object is used in static import, so it has a structure of HasPropertyWithValue.hasProperty
to be exact.
And this method has the following roles. (Quoted from the official)
Creates a matcher that matches when the examined object has a JavaBean property with the specified name whose value satisfies the specified matcher.
Somehow an esoteric English sentence came out, but I think that it will come nicely if you look at an actual example.
assertThat(myBean, hasProperty("foo", equalTo("bar"))
This means that the object "myBean" has a property called "foo" and the value of the foo property is "bar". That's exactly what we want to verify in this test.
A simple example of this example would be ʻassertThat (user, hasProperty ("userName", is ("test0")); `.
Since the return value of hasProperty belongs to the Matcher type, you can also write the property in a nested manner.
Verification of nested properties inevitably lengthens the code and makes it difficult to understand the correspondence between parentheses, so I think it is necessary to take some measures to make it easier to read, such as devising indentation as in the above example.
Anyway, this makes it possible to handle complicated models. As an example of the final Model, let's take a look at the test code for the List object.
Let's look at the final Model pattern, which is nested and has a list structure.
As a common example of model.addAttribute ("dbForm ", form);
, verify that the "list of users in the Form object" is what you expect.
As an example, first write the code below.
HelloControllerTest.java(Excerpt)
//For list elements, access the list in any order with hasItem and verify whether there is an element whose specified property has the specified value.
//Make test green only if present
@Test
User list is stored in the model form by void init processing() throws Exception {
this.mockMvc.perform(get("/hello/init"))
.andExpect(model().attribute("dbForm", hasProperty(
"userList", hasItem(
hasProperty(
"userName", is("test1")
)
)
)));
}
A new method called hasItem
has appeared.
Official It is said that it can be used only for list format objects. I am.
And the method used here has Matcher as an argument.
In other words, roughly speaking, we are verifying that there is at least one that satisfies the Matcher passed as an argument in the list to be executed by the hasItem method. In this example, we want to verify that for each user element in the user list, there is at least one with the "userName" property set to "test1".
Since the list in the example has a small number of elements such as "2", it is possible to examine all the contents, but in the actual application, a list packed with hundreds or thousands of elements is often passed. .. It's hard to verify all this, though it can be coded. In such a case, it seems that reliability can be guaranteed to some extent if it is possible to verify whether the elements near the beginning, middle, and end meet the specifications. Therefore, instead of verifying all the elements of the list, it is only necessary to verify only a part as a representative element, so I think it is better to provide some test cases with the hasItem method.
By the way, it has become quite long because it was mixed with various supplementary explanations, but the level 1 test code is now verified. By completing Level 1, you can do the following:
Next, at Level 2, I would like to look at the verification of the database, which is the core of the application.
In the unit test with Spring Boot, we use something called "DbUnit" to verify the database. If you write it like this, even though Spring Boot alone is full, you will have to study more ... but I think that it is enough to learn how to use DbUnit easily. The important thing is to be aware of "what kind of work will be easier" by combining DbUnit and Spring Boot.
If you suddenly write about DbUnit in a mess, the image will be difficult to understand, so let's first follow the flow of how database operations are replaced from manual tests to automated tests.
First, consider manually testing your database operations. I think that the test will proceed in the following flow.
Execute the process to get the record by SELECT from the database on the application side
Verify that the acquisition result is as expected while looking at the acquired record
Executes the process of UPDATE and DELETE of database records on the application side
Compare the records in the database before and after applying the process and verify that the result is as expected.
The pros and cons of manual testing are left here, but here we will focus on "test reproducibility". If you are developing as a team, the result of SELECT will change from moment to moment, and UPDATE and DELETE processing will require some advance preparation if you want to meet the same conditions. If the tests executed are not reproducible, it will be difficult to identify whether or not degradation has actually occurred when repeated tests such as refactoring and regression tests are performed.
Now let's devise a little more about manual testing.
In order to ensure reproducibility, I tried to incorporate the following process into the above manual test.
This ensured the reproducibility of the test. If this is the case, you can test with confidence ... !! It may not be possible if it is a small application, but if you back up the entire database, delete the entire database, restore the entire database, etc. every time you do a little verification, it will be a test. It will take a huge amount of time.
Even if you create a reproducible test state, if it takes a long time to execute the test itself or return the result, the test will not be executed at the right time. I just wanted to debug a bit, but if I wasn't good enough, I would have to wait for tens of minutes before the results came back, and if I wasn't good enough, I would end up putting it off and returning to a manual tick test.
So far, testing database operations while ensuring reproducibility may seem like a bit of a hurdle. However, by combining Spring Boot and DbUnit, although some preparation is required, the above tests can be executed with the touch of a button.
Now, let's take a look at the test code using Spring Boot and DbUnit as the main subject of Level 2.
First, let's look at the process of getting the result with the royal road SELECT. The code of the Dao layer to be verified is described below.
UserDao.java(Excerpt)
/**
*Get all user records from DB
*This time, for testing, the process was simplified.
* @return List of user entities
*/
public List<User> findAllUser() {
QueryBuilder query = new QueryBuilder();
query.append("select user_id, user_name from tm_user");
return findResultList(query.createQuery(User.class, getEm()));
}
Various processes are written, but the following two points should be noted.
The process of SELECTing records from the database will continue to recur, so the first step is to verify number of records in the database = list size
.
I will use DbUnit immediately for verification, but some preparation is required before writing the actual test code. There are a lot of things to do in preparation, but once you master it, you can make it a routine for subsequent database operation tests, so I'd like to take a closer look.
First, we'll lay the groundwork for managing database records in files. As a standard function of DbUnit, record transaction settings etc. are described in XML file, but this time we will manage records in CSV. There are various reasons, but in summary, the big thing is that you can write simply.
I'll take a few steps below, but all of them are simple, so I think you can understand them intuitively to some extent without going too far into DbUnit itself.
So, first of all, we will create a class to use the CSV file in the test.
CsvDataSetLoader
CsvDataSetLoader.class
public class CsvDataSetLoader extends AbstractDataSetLoader{
@Override
protected IDataSet createDataSet(Resource resource) throws Exception {
return new CsvURLDataSet(resource.getURL());
}
}
The following is a supplementary explanation of the important elements.
Literally an abstract class for reading some dataset. The dataset here stands for "a set of tables". Since this abstract class is an implementation class of the DataSetLoader interface, the class to be created will be of type "DataSetLoader". In other words, if you look at the class created at the class level, it is as simple as describing the information "This is a class for reading a dataset".
Again, as the name implies, it's a factory method for creating datasets. The Resource type "resouce" object passed as an argument has information and behavior for accessing the "real file". In the actual test, the resource object is in the form of storing the path of the CSV file to be processed.
In Official, This class constructs an IDataSet given a base URL containing CSV files
As you can see, by getting the actual CSV file based on the above resource object and converting it to a dataset object, DbUnit can process it.
Some processing was written, but as the class name implies, this class is for reading the actual CSV file and making it available for testing database operations.
Once written, it can be used when testing database operations using CSV files in other apps, so here, if you can get an overview of what each process represents, the problem is. I think there isn't.
Now that the class for reading CSV has been completed, let's create a CSV file to be actually read. For a sample file, go to GitHub.
There are various ways to create the CSV file itself, but personally, it is recommended to use DBeaver to extract the CSV from the record and use it as it is. The following points should be noted when creating a CSV file.
You also need to be careful where you put the CSV files.
(I put it in a strange place and got hooked.)
Basically, it will be placed under src / test / resources
.
Reference
The specific file / folder structure is as follows.
You can see that there is a CSV file that looks like a table name under src / test / resources / testData
.
And next to it is an unfamiliar text file called table-ordering.txt
.
This is to prevent foreign key constraints and specifies the order in which the database tables are read.
The specific writing method is as follows.
table-ordering.txt
TableA
TableB
TableC
Now that we're finally ready, we can get into the test code. The number of annotations will suddenly increase, but if you overcome this, the range in which you can write tests will expand dramatically, so I will do my best.
DBSelectTest.java
@DbUnitConfiguration(dataSetLoader = CsvDataSetLoader.class)
@TestExecutionListeners({
DependencyInjectionTestExecutionListener.class,
TransactionalTestExecutionListener.class,
DbUnitTestExecutionListener.class
})
@SpringBootTest(classes = {DaoTestApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class DBSelectTest {
@Autowired
private UserDao userDao;
//By setting the CSV file path in the value of DatabaseSetup, "table"-ordering.See txt "
//Create a test table group by creating a sequential table
//At this time, the path of value is "src"/test/Starting from under "resources", the file name of the CSV file is
//Correspond to the name of the table
//
//Also,@By adding the Transactional annotation, you can roll back the transaction when the test is finished.
//Can be tested without polluting the environment
@Test
@DatabaseSetup(value = "/testData/")
@Transactional
public void contextLoads() throws Exception {
List<User> userList = userDao.findAllUser();
//Did Dao successfully get records from the table?
assertThat(userList.size(), is(2));
}
}
In order to get the whole picture, we will start with the annotations given to the class.
DbUnitConfiguration
As you read, it is an annotation for setting various settings of DbUnit.
By specifying the CsvDataSetLoader
created above in the" dataSetLoader "property, you can read the CSV file.
There seem to be various other settings, but at this stage I think there is no problem with the recognition that it is used to load CSV.
TestExecutionListeners
This is the most difficult part of this test code. I think this annotation should be enough to give you an overview and sort out what should be passed to the property.
As an overview, it is for loading the necessary one of the TestExecutionListener
, which defines the processing to be performed before and after the execution of the test code.
The description of each listener is roughly summarized in Official. , Here, I will briefly describe the ones that are often used.
Specify when using DI in the test code. By specifying, it is possible to inject the test target class from the DI container with Autowired etc.
Specify when setting the transaction for DB operation. After operating the DB, it is basic to return the DB to its original state, so it is basically essential for tests dealing with the DB.
Specify when setting the state of the database before and after the test with the annotation described later. As the name suggests, when using DbUnit, basically add it.
SpringBootTest
Well, this is the second appearance. Here, a new webEnvironment
property is passed.
"MOCK" is set as the default value, and it seems that you are creating something called mock servlet environment
. The details were not officially written much, but as far as the console output is seen, it seems to represent the process of generating the DispatcherServlet for testing used by MockMvc.
Since it is not necessary to exchange request / response with the server in the Dao layer alone, set "NONE" as the property value. This cuts the process of creating the context for MockMvc and makes the test finish a little faster.
And although the order has been reversed, what is passed to the classes property has also changed. That doesn't mean that the process has changed significantly, it just narrows the scope of component scans. If you look at the actual code, it will come to you.
DaoTestApplication.java
@EntityScan("app.db.entity")
@SpringBootApplication(scanBasePackages = "app.db.dao")
public class DaoTestApplication {
public static void main(String[] args) {
SpringApplication.run(DaoTestApplication.class, args);
}
}
The target to be read is limited to "Dao layer" and "Entity handled by Dao". As the size of the app grows, it takes longer to load the package, and it takes longer to load. Since the processing related to database operations is changed frequently and we want to run the test code as much as possible, we have minimized the reading range to shorten the time as much as possible.
With this kind of ingenuity, the test will be completed in about one turn of the neck after stretching. If you want more speed, you'll have to mess around with the Config class and mess around with EntityManager, but it's not too fatally slow to test, so don't go that far.
The above settings can be done quickly, and the effect can be obtained as it is, so I think that this is enough at the basic stage.
It's about annotations again, but it's simpler than class-level annotations, so I think it'll come to your mind.
DatabaseSetup
This is an annotation to define the "initial state" of the database. If you specify the directory where the CSV file is located in the value property, the value will be packed in the database table based on the CSV file. You can also create different states by switching directories.
This allows you to always reproduce the state of the table when you want to start the test, without having to manually insert it into the table. Thank you.
Transactional
It is a familiar one that is often used in actual application development. Normally, a method with this annotation behaves like committing if it works normally, rolling back if it does something unexpected, and so on.
However, in the case of test code, if you rewrite the database after each test, the reproducibility of the test will be lost, so by default it will be rolled back every time the method is executed.
By combining the above two annotations, you can manually back up the database, stuff records from files, put them back at the end, and so on.
When you actually run the test, you can get the list of user entities with Dao's method and verify that you get the expected result (list size = number of records).
By verifying the SELECT process, I was able to cover the basics of the database operation test code to some extent, so I would like to take a look at other processes at once.
Of the CRUD processing, we were able to verify SELECT, so let's look at the remaining update / creation processing. I think that you can understand the outline with the knowledge you have acquired so far, so the actual test code is described below.
CRUDDaoTest.java(Excerpt)
//Processing to reflect the state after executing the test method in the database
//Normally, update processing is synchronized with the database when the transaction is committed,
//Explicitly synchronize because it does not commit in the test process
@AfterEach
void tearDown() {
userDao.getEm().flush();
}
/**
*Verify if a new record is created by the create process
*Verify that the DB was rewritten as expected by the entity by comparing it with the Expected Database
*/
@Test
@DatabaseSetup(value = "/CRUD/setUp/forCreate")
@ExpectedDatabase(value = "/CRUD/create/", assertionMode=DatabaseAssertionMode.NON_STRICT)
A new user is created with the void create method() {
User user = new User();
user.setUserName("test3");
userDao.saveOrUpdate(user);
}
/**
*Verify if the existing record is updated by the update process
*Verify that the DB was rewritten as expected by the entity by comparing it with the Expected Database
*/
@Test
@DatabaseSetup(value = "/CRUD/setUp/")
@ExpectedDatabase(value = "/CRUD/update/", assertionMode=DatabaseAssertionMode.NON_STRICT)
User 1 can be rewritten with the void update method() {
User user = new User();
user.setUserId(1L);
user.setUserName("test1mod");
userDao.saveOrUpdate(user);
}
/**
*Verify if the record is deleted by the delete process
*Prepare a DB before and after processing, and verify the validity by comparing whether the expected result will be obtained after deletion.
*/
@Test
@DatabaseSetup(value = "/CRUD/setUp/")
@ExpectedDatabase(value = "/CRUD/delete/", assertionMode=DatabaseAssertionMode.NON_STRICT)
User 1 can be deleted with the void delete method() {
userDao.delete(1);
}
Now that we have some new ones, let's take a quick look at each one. Also, there are some caveats about the test code for CURD processing of database operations, so let's take a look at those as well.
AfterEach
This is an annotation for JUnit5 and describes the process you want to insert after executing each test method. Here, the flush method of EntityManager is explicitly called. The flush method is doing the work to synchronize the entities in the persistence context with the records in the database. Normally, this method is automatically called when the transaction is committed, without even being aware of it. Reference
However, in this test code, we need to RollBack
to restore the database after the database operation is finished. Then, the flush method will not be called, so the expected result of the test method will not be reflected in the database, and the test will not pass.
There are several ways to deal with it, but it seems better to explicitly call the flush method when each method commits a transaction, that is, when the process is completed, just like running an app.
From the above, by calling the flush method after executing each test method, the expected result can be verified correctly.
ExpectedDatabase
It is used at the same time as the DatabaseSetup annotation, and as the name implies, it is for verifying the state of the database after executing the test method. As with the DatabaseSetup annotation, specify the directory where the CSV file that describes the table state of the expected result is stored in the value value. Furthermore, the property "assertionMode" is set, but by setting "NON_STRICT" here, only the columns specified in the CSV file will be verified, not all columns.
In this test class, Transactional annotation
is added at the class level.
Setting this annotation at the class level is the same as annotating all the methods in the class.
In some Controller tests, transaction control is not required, but if you try to set each method every time, omissions will occur, so it is better to set them collectively at the class level.
Now you should have some understanding of the processing required for CRUD processing. At the end of Level 2, here are some things to keep in mind when testing database operations (because I'm addicted to it). I hope it will be helpful for you.
The above process is a process to rewrite an existing record. I think that it may process multiple records, but in many cases, Web applications will target one record. At this time, key information is required to clarify the processing target.
I think there are various ways to do it, but I think it's simple and easy to write "specify the ID in the record of the CSV file". The point to note here is the concept of logic to realize the test. Since it seems to be a little long, I wrote it in the supplement, so please take a look if you are interested.
Consider the case of registering a new record in the database with a test method. For example, if the ID is automatically numbered, and the ID is assigned to the setup record, key duplication may occur.
In that case, if you set the ID that the record of the result set is also numbered ..., it is safer not to control the value that is automatically numbered.
As a solution, if you want to verify the generation of a new record, I think it is better to proceed with the policy of using a CSV file excluding the ID and verifying only the contents column without the ID involved.
A lot of new things have come out in level 2. However, if you get used to it to some extent, you can write crisply, and above all, start the server like a manual test, access the page, actually process it and go to see the DB ... I think that it is a very beneficial part because it can be verified without doing it. Therefore, even if you master the range up to level 2, the efficiency of defect correction during development will be greatly improved.
By completing Level 2, you should be able to:
By the way, Spring Boot is a framework for creating Web applications, so it is common to use POST requests when actually operating the database. Therefore, at Level 3, we would like to take a look at POST request validation. The level will go up, but it's something that you can fully understand with your knowledge so far, so I'd be happy if you could follow me to the end (╹◡╹).
Next, let's look at the test code for validating POST requests. Since it would be long to put the entire code to be verified, I would like to focus on the test code by describing only the outline of the application here.
Level 3 uses a to-do list as a subject. It's a simple one that can do the following simple CRUD processing.
There will be some new knowledge about POST requests, but if you have the knowledge so far, you can understand it, so please take a look at the test code after a comprehensive review of this article. The actual test code is shown below. It's a bit long, but most of it is understandable ... I'm happy.
TodoControllerTest.java
@DbUnitConfiguration(dataSetLoader = CsvDataSetLoader.class)
@TestExecutionListeners({
DependencyInjectionTestExecutionListener.class,
TransactionalTestExecutionListener.class,
DbUnitTestExecutionListener.class
})
@AutoConfigureMockMvc
@SpringBootTest(classes = {DbMvcTestApplication.class})
@Transactional
public class TodoControllerTest {
//mockMvc Mock object for handling Http request and response without deploying to Tomcat server
@Autowired
private MockMvc mockMvc;
@Autowired
private TodoDao todoDao;
@AfterEach
void tearDown() {
todoDao.getEm().flush();
}
/**
*Verify that view is returned correctly
* @throws Exception
*/
@Test
Todo is passed as view in void init processing() throws Exception {
this.mockMvc.perform(get("/todo/init"))
.andExpect(status().isOk())
.andExpect(view().name("todo"));
}
/**
*Verify if the record acquired from DB is set in the model
*This time it is not a complicated process, so if one record in the DB is passed to the model, it is considered to be operating normally.
*
* @throws Exception
*/
@Test
@DatabaseSetup(value = "/TODO/setUp/")
The void init process passes the existing task to the model() throws Exception {
//With mockMvc/todo/Send a get request to "init"
this.mockMvc.perform(get("/todo/init"))
//DB records are passed to the model as a list
.andExpect(model().attribute("todoForm", hasProperty(
"todoList", hasItem(
hasProperty(
"task", is("task1")
)
)
)));
}
/**
*Verify if new record is registered in DB from screen input
* @throws Exception
*/
@Test
@DatabaseSetup(value = "/TODO/setUp/create")
@ExpectedDatabase(value = "/TODO/create/", assertionMode=DatabaseAssertionMode.NON_STRICT)
A new task is registered in the DB by void save processing() throws Exception {
this.mockMvc.perform(post("/todo/save")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("newTask", "newTask"));
}
/**
*Verify if existing records are updated by screen input
*Since screen information is not used this time, it is not possible to obtain the ID that is automatically numbered.
*Therefore, this time, specify the update target manually.
*Basically, the order of the list is not guaranteed, so it seems necessary to sort it at the time of SELECT.
* @throws Exception
*/
@Test
@DatabaseSetup(value = "/TODO/setUp/")
@ExpectedDatabase(value = "/TODO/update/", assertionMode=DatabaseAssertionMode.NON_STRICT)
The void update process updates the existing task() throws Exception{
//"To do" with mockMvc/Send a post request to "update"
long updateTargetId = 3L;
int updateTargetIndex = 2;
this.mockMvc.perform(post("/todo/update/" + updateTargetIndex + "/" + updateTargetId)
.param("todoList[" + updateTargetIndex + "].task", "task3mod")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
);
}
/**
*Verify if the task selected on the screen is deleted
* @throws Exception
*/
@Test
@DatabaseSetup(value = "/TODO/setUp/")
@ExpectedDatabase(value = "/TODO/delete/", assertionMode=DatabaseAssertionMode.NON_STRICT)
The void delete process deletes the existing task() throws Exception {
long deleteTargetId = 3L;
this.mockMvc.perform(post("/todo/delete/" + deleteTargetId)
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
);
}
}
In the following, we will describe the new POST request related methods and points to note when handling POST requests. I don't think it's scary because what you're doing is just setting parameters and making requests.
A notable point in the POST request test code is how to set the parameters passed to the request. However, if you understand the POST request to some extent, you can set it intuitively. For POST requests, we recommend it because it is clearly written in MDN.
By the way, in the test code using MockMvc
, the part where the GET request was made by the perform method at level 1 will be changed to the POST request.
After that, you can pass parameters in key-value format with the param method, so you can just make a request according to the form passed in the actual request.
You can also call the param method in a GET request, but in that case it will be sent as a query parameter. Here, the POST request is submitted based on the form, so the parameters are stored in the request body.
Also, although the contentType works without specifying it, I think it is better to set it so that it is as close as possible to the actual POST request.
This time, we are mainly verifying whether the database is updated correctly by the POST request. At this point, the problem is what is processed by the POST request. I would like to take a quick look at each CRUD process.
When creating a new record, the parameters for the new record are independent of the table, so you shouldn't worry about it.
This time, the verification range is until the view name is passed to View, so it is OK if you can verify the object passed to Model. So there's nothing to worry about here either ...
Since the ID to be deleted is determined by the path of the request, it is necessary to clearly indicate the deletion target when creating a request with MockMvc. You should avoid these conventions in your "implementation", but I think you don't have to worry too much about them in your test code. In the first place, the test code defines the state of the database as "fixed", so rather than assuming changes and expansions, you should focus on the part of "whether a constant output is always obtained from a constant input". The strength of test code is that you can always get the same result no matter how many times you execute it, so I personally think that you should think about how to write it separately from implementation.
The same can be said for the update process. However, when updating, if you want to target one record in the list of entities, you need to take some measures such as separating the edit target from the list and storing it in a separate update entity. In this update process, the ID of the index entity of the list is fixed, but the order of the list is basically not guaranteed, so in the business level application, in the above form, "always It is necessary to create a state where the same result can be obtained from the same input. I would like to write about that when I get used to the screen test code (desire).
Although it was labeled as level 3, most of it was in the form of a comprehensive review so far, so I understand ... I understand ... !! I would be very happy if you could (:) Understanding Level 3 test code allows you to:
It's been longer than I expected, but now I've seen how to write test code for simple CRUD processing. When writing the implementation part, it is necessary to be aware of things that you would not normally be aware of, and I think there were some hard parts. However, writing test code will deepen your understanding of frameworks and languages, streamline development, and provide many benefits.
And best of all, the joy of passing a test is something you can't enjoy with implementation alone, and once you've overcome the first barrier, writing test code becomes a lot of fun.
Through this article, I might be able to write test code with Spring Boot ...? I would appreciate it if you could think. I'm still immature about test code, so I'd like to see more explanations about test code.
Recommended Posts