Spring Boot Configuration Tutorial
1. Introduction
When you first heard about Spring Boot, I am sure your wondered what it is for and what is the advantage of using it. So did I.
Spring Boot as the name suggests handles the bootstrapping of a Spring application with a minimal Spring configuration and thus making the application development quicker and simpler. It comes with a set of starter POMs you can choose from. Based on the starter POM you had selected to use, Spring Boot resolves and downloads an assumed set of dependencies. Thus the developer can focus on developing the business logic while Spring Boot handles the starter Spring configuration required.
In this tutorial, you are going to learn how to use Spring Boot with help of a sample “Store Management” CRUD application.
Table Of Contents
2. Environment
This tutorial assumes that you have basic understanding of Java 1.8, Gradle 2.9, Eclipse IDE (Luna) and Spring framework. Please make sure you have a working environment ready using the following technologies, before you attempt to develop/run the “Store Management” application.
If you had never used these technologies before or do not have a working environment, I would recommend you to please follow the links provided below to secure required knowledge and get your environment up and running, before you proceed with this tutorial.
- Java 1.8
- Gradle 2.9
- Eclipse IDE (Luna)
- Eclipse Buildship Plugin for Eclipse Gradle integration
- Spring framework
- ThymeLeaf
- Mockito
- JUnit
- MYSQL
- Spring Test Framework
In addition to the above, the following technologies are used in this tutorial.
3. The “Store Management” Application
3.1. Create and configure a Gradle project in Eclipse IDE
If you had never created a Gradle project using Eclipse IDE, I would recommend you to refer to my previous tutorial Spock Tutorial For Beginners that gives you detailed steps on how to create Gradle Project in Eclipse IDE.
The following is the project structure after creating the Gradle Project and the required java/resource files.
3.2 build.gradle – Quick Walk through
In the Eclipse IDE, open the build.gradle
file that is in the project root directory. Update the file as shown below.
build.gradle
buildscript { repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:1.3.3.RELEASE") } } apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'spring-boot' jar { baseName = 'store_management' version = '0.1.0' } repositories { mavenCentral() } sourceCompatibility = 1.8 targetCompatibility = 1.8 sourceSets { main { java.srcDir "src/main/java" resources.srcDir "src/main/resources" } test { java.srcDir "src/test/java" resources.srcDir "src/test/resources" } integrationTest { java.srcDir "src/integrationTest/java" resources.srcDir "src/integrationTest/resources" compileClasspath += main.output + test.output runtimeClasspath += main.output + test.output } } configurations { integrationTestCompile.extendsFrom testCompile integrationTestRuntime.extendsFrom testRuntime } dependencies { testCompile("org.springframework.boot:spring-boot-starter-test") compile("org.springframework.boot:spring-boot-starter-data-jpa") compile("org.springframework.boot:spring-boot-starter-thymeleaf") compile("mysql:mysql-connector-java:5.1.38") } task integrationTest(type: Test) { testClassesDir = sourceSets.integrationTest.output.classesDir classpath = sourceSets.integrationTest.runtimeClasspath outputs.upToDateWhen { false } } check.dependsOn integrationTest integrationTest.mustRunAfter test tasks.withType(Test) { reports.html.destination = file("${reporting.baseDir}/${name}") }
Let us quickly walk through this build.gradle.
buildscript { repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:1.3.3.RELEASE") } }
buildscript
is used to add the external dependencies to buildscript classpath. A closure that declares build script classpath and adds dependencies to classpath configuration is passed to buildscript()
method.
apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'spring-boot'
To apply the required plugins java
,eclipse
and spring-boot
so the associated tasks can be used in the build script as needed.
jar { baseName = 'store_management' version = '0.1.0' }
A jar with the name store_management-0.1.0.jar
is created under build/libs
folder. You may run the Spring Boot Application using the following command:
gradlew build && java -jar build/libs/store_management-0.1.0.jar
repositories { mavenCentral() }
This closure is used to specify the repositories from where the required dependencies are downloaded from.
sourceCompatibility = 1.8 targetCompatibility = 1.8
SourceCompatibility
is the Java version compatibility to use when compiling Java source.TargetCompatibility
is the Java version to generate classes for.
sourceSets { main { java.srcDir "src/main/java" resources.srcDir "src/main/resources" } test { java.srcDir "src/test/java" resources.srcDir "src/test/resources" } integrationtest { java.srcDir "src/integrationtest/java" resources.srcDir "src/integrationtest/resources" compileClasspath += main.output + test.output runtimeClasspath += main.output + test.output } }
sourceSets
is used to group the source files into logical groups. The source files can be java or resource files. This plugin also has associated compileClasspath and runtimeClasspath.
dependencies { testCompile("org.springframework.boot:spring-boot-starter-test") compile("org.springframework.boot:spring-boot-starter-data-jpa") compile("org.springframework.boot:spring-boot-starter-thymeleaf") compile("mysql:mysql-connector-java:5.1.38") }
This is to define the required dependencies needed for this tutorial. As you had seen we have configured the starter POMs for test, JPA and Thymeleaf. Spring Boot, based on the starter POMs defined, resolves the assumed set of dependencies as shown in the picture below. MySQL is used as database for both integration tests and as the production database.
configurations { integrationtestCompile.extendsFrom testCompile integrationtestRuntime.extendsFrom testRuntime }
The integrationtestCompile
dependency configuration inherits the dependency configuration required to compile the unit tests. The integrationtestRuntime
dependency configuration inherits the dependency configuration required to run the unit tests.
task integrationtest(type: Test) { testClassesDir = sourceSets.integrationtest.output.classesDir classpath = sourceSets.integrationtest.runtimeClasspath outputs.upToDateWhen { false } }
testClassesDir
is set to configure the location for the integration test classes. classpath
specifies the classpath used when integration tests are run. outputs.upToDateWhen { false }
is set to false so that the integration tests are executed everytime the integrationtest
task is invoked.
check.dependsOn integrationtest integrationtest.mustRunAfter test
As you are aware, build
task is combination of check
and assemble
tasks. As it is self-explanatory, check.dependsOn integrationtest
is to make sure integration tests are run when the build task is invoked. integrationtest.mustRunAfter test
is to make sure that the unit tests are run before integration test.
tasks.withType(Test) { reports.html.destination = file("${reporting.baseDir}/${name}") }
This is to make sure the unit test and integration test reports are written to different directories.
While searching online for help to efficiently configure the integration tests, I had stumbled upon the following quite useful links.
3.3 The CRUD
StoreManagementApplication.java
package management; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class StoreManagementApplication { public static void main(String[] args) { SpringApplication.run(StoreManagementApplication.class, args); } }
This is the entry point of the Spring Boot Application. @SpringBootApplication is combination of the annotations @Configuration
, @EnableAutoConfiguration
and @ComponentScan
.
AppInitializer.java
package management.store.config; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRegistration; import org.springframework.web.WebApplicationInitializer; import org.springframework.web.context.ContextLoaderListener; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.servlet.DispatcherServlet; public class AppInitializer implements WebApplicationInitializer { @Override public void onStartup(ServletContext servletContext) throws ServletException { WebApplicationContext context = getContext(); servletContext.addListener(new ContextLoaderListener(context)); ServletRegistration.Dynamic dispatcher = servletContext.addServlet("DispatcherServlet", new DispatcherServlet(context)); dispatcher.setLoadOnStartup(1); dispatcher.addMapping("/*"); } private AnnotationConfigWebApplicationContext getContext() { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); context.setConfigLocation("management.store.config"); return context; } }
Have you noticed yet, we haven’t created any web.xml at all?
The AppInitializer
class configures the required ServletContext programmatically by implementing the interface WebApplicationInitializer
thus removing the need to create any web.xml.
The onStartup()
is implemented to configure the given ServletContext with any servlets, filters, listeners context-params and attributes necessary for initializing this web application.
The addServlet()
registers an instance of DispatcherServlet
to be used with ServletContext
.
The AnnotationConfigWebApplicationContext
is implmentation of WebApplicationContext
which scans and accepts classes annotated with @Configuration
in the classpath configured by setConfigLocation()
. As you can see, we have configured the location as management.store.config
, where all the @configuration
annotated classes are stored.
WebConfig.java
package management.store.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import org.thymeleaf.spring4.SpringTemplateEngine; import org.thymeleaf.spring4.view.ThymeleafViewResolver; import org.thymeleaf.templateresolver.ServletContextTemplateResolver; import org.thymeleaf.templateresolver.TemplateResolver; @EnableWebMvc @Configuration @ComponentScan(basePackages = "management.store.config") public class WebConfig extends WebMvcConfigurerAdapter { @Bean public TemplateResolver templateResolver(){ ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver(); templateResolver.setPrefix("/WEB-INF/view/"); templateResolver.setSuffix(".html"); templateResolver.setTemplateMode("HTML5"); return templateResolver; } @Bean public SpringTemplateEngine templateEngine() { SpringTemplateEngine templateEngine = new SpringTemplateEngine(); templateEngine.setTemplateResolver(templateResolver()); return templateEngine; } @Bean public ViewResolver getViewResolver() { ThymeleafViewResolver resolver = new ThymeleafViewResolver(); resolver.setTemplateEngine(templateEngine()); resolver.setOrder(1); return resolver; } }
You might have already noticed that we haven’t created any xml for Spring MVC configuration. The above class provides the Spring MVC configuration programmatically. In our current example, we have configured the ServletContextTemplateResolver
with the required details such as resource location (WEB-INF/view
) and the type of resource (.html
) to resolve the resources.
BaseController.java
package management.store.controller; public class BaseController { }
This is the base class for our controller hierarchy.
StoreManagementController.java
package management.store.controller; import management.store.model.Store; import management.store.service.StoreManagementService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller public class StoreManagementController extends BaseController{ @Autowired StoreManagementService storeService; @RequestMapping(value = "/loadstore", method = RequestMethod.GET) public String storeLoad(Model model) { model.addAttribute("store", new Store()); return "store"; } @RequestMapping(value = "/getallstores", method = RequestMethod.GET) public String getAllStores(Model model) { model.addAttribute("stores", storeService.getAllStores()); return "storelist"; } @RequestMapping(value = "/addstore", method = RequestMethod.POST) public String storeAdd(@ModelAttribute Store store, Model model) { Store addedStore = storeService.addStore(store); model.addAttribute("stores", storeService.getAllStores()); return "storelist"; } @RequestMapping(value = "/deletestore/{id}", method = RequestMethod.GET) public String storeDelete(@PathVariable Long id, Model model) { storeService.deleteStore(id); model.addAttribute("stores", storeService.getAllStores()); return "storelist"; } @RequestMapping(value = "/updatestore", method = RequestMethod.POST) public String storeUpdate(@ModelAttribute Store store, Model model) { storeService.updateStore(store); model.addAttribute("stores", storeService.getAllStores()); return "storelist"; } @RequestMapping(value = "/editstore/{id}", method = RequestMethod.GET) public String storeEdit(@PathVariable Long id, Model model) { model.addAttribute("store", storeService.getStore(id)); return "editstore"; } }
@Controller
stereotype annotation indicates that the class is a “Controller” (e.g. a web controller). Service is autowired into the controller. The controller invokes the service methods to perform the required CRUD operations on the database.@RequestMapping
is used to map the web requests onto specific handler classes and/or handler methods. As shown in the above example the request/loadstore
is mapped to the methodstoreLoad
.RequestMethod.GET
is to specify that this is a GET request.@ModelAttribute
maps the named model attribute that is exposed to the webview, to the method parameter on which the annotation is defined.@PathVariable
maps a method parameter to a URI template variable.
Store.java
package management.store.model; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.SequenceGenerator; @Entity public class Store { @Id @GeneratedValue(generator="STORE_SEQ") @SequenceGenerator(name="STORE_SEQ",sequenceName="STORE_SEQ", allocationSize=1) Long storeId; String storeName; String storeStreetAddress; String storeSuburb; String storePostcode; public Long getStoreId() { return storeId; } public void setStoreId(Long storeId) { this.storeId = storeId; } public String getStoreName() { return storeName; } public void setStoreName(String storeName) { this.storeName = storeName; } public String getStoreStreetAddress() { return storeStreetAddress; } public void setStoreStreetAddress(String storeStreetAddress) { this.storeStreetAddress = storeStreetAddress; } public String getStoreSuburb() { return storeSuburb; } public void setStoreSuburb(String storeSuburb) { this.storeSuburb = storeSuburb; } public String getStorePostcode() { return storePostcode; } public void setStorePostcode(String storePostcode) { this.storePostcode = storePostcode; } }
The @Entity
is the entity class mapped to the corresponding table in the database. The @Id
is used to specify the primary key of the entity. @GeneratedValue
specifies generation strategy for the primary key field. In this case it is a sequence generated using @SequenceGenerator
.A @SequenceGenerator
may be specified on the entity class or on the primary key field or property.
StoreRepository.java
package management.store.repo; import management.store.model.Store; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; @Repository public interface StoreRepository extends CrudRepository { }
The @Repository
stereotype annotation is to denote the interface to be a repository.
CrudRepository
is interface for generic CRUD operations on a repository. The types specified are the type of the entity (in our case Store
) and the type of the primary key field(Long
in this example).
StoreManagementService.java
package management.store.service; import java.util.List; import management.store.model.Store; public interface StoreManagementService { public Store addStore(Store store); public List getAllStores(); public Store getStore(Long id); public Store updateStore(Store store); public void deleteStore(Long id); }
This is the parent interface for our service hierachy.
StoreManagementServiceImpl.java
package management.store.service; import java.util.ArrayList; import java.util.List; import management.store.model.Store; import management.store.repo.StoreRepository; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class StoreManagementServiceImpl implements StoreManagementService { @Autowired StoreRepository storeRepository; @Override public Store addStore(Store store) { if (store == null) throw new IllegalArgumentException("Store is null"); return storeRepository.save(store); } @Override public Store updateStore(Store store) { if (store == null) throw new IllegalArgumentException("Store is null"); Store currentStore = getStore(store.getStoreId()); if (currentStore == null) throw new IllegalArgumentException( "Store doesnot exist with given store id"); BeanUtils.copyProperties(store, currentStore); return storeRepository.save(currentStore); } @Override public Store getStore(Long id) { if (id == null) throw new IllegalArgumentException("Store Id is null"); Store st = storeRepository.findOne(id); if (st == null) throw new IllegalArgumentException("Store with given store id does not exist"); return st; } @Override public List getAllStores() { List list = new ArrayList(); storeRepository.findAll().forEach(list::add); return list; } @Override public void deleteStore(Long id) { if (id == null) throw new IllegalArgumentException("Store Id is null"); if (getStore(id) != null) storeRepository.delete(id); } }
This is the implementation of the parent interface StoreManagementService
. The methods are implemented by invoking the methods on StoreRepository
that is autowired into the service.
application.properties
spring.datasource.platform=mysql spring.jpa.show-sql=false spring.jpa.hibernate.ddl-auto=update spring.datasource.driverClassName=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://127.0.0.1:3306/myarticledb spring.datasource.username=srujana spring.datasource.password=nimda
schema-mysql.sql
--Integration test also uses MySql database --To clear the test data created by schema-mysql-test.sql delete from store where store_name in ("S1", "S2", "S3", "S4", "S5", "S6");
This is the configuration used by the application to connect to MYSQl database. Based on the value XXX configured for spring.datasource.platform
SpringApplication looks for and uses the corresponding schema-XXX.sql
file to run against the database. For ex. the value for spring.datasource.platform
is “mysql” and thus the schema-mysql.sql
file is executed when the Spring Boot Application is run.
Here in the schema-mysql.sql
we are issuing a delete
command. Did you figure out why? Yes, you are right. In our tutorial, as you can see in application-test.properties
the integration tests are also configured to use the same database as the production application. Thus before running the production application we are trying to sanitize the production database by removing the testdata. This hassle of explicit clearing of the test data can be overcome by configuring the integration tests to use an embedded database such as h2 while production application can be configured to use a separate database such as MySQL.
editstore.html
<!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Store Management</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> </head> <body> <h1>Store Management</h1> <form action="#" th:action="@{/updatestore}" th:object="${store}" method="post"> <table> <tr> <td>Store Id:</td> <td><input type="text" th:field="*{storeId}" readonly="readonly" /></td> </tr> <tr> <td>Store Name:</td> <td><input type="text" th:field="*{storeName}" /></td> </tr> <tr> <td>Store Street Address :</td> <td><input type="text" th:field="*{storeStreetAddress}" /></td> </tr> <tr> <td>Store Suburb:</td> <td><input type="text" th:field="*{storeSuburb}" /></td> </tr> <tr> <td>Store PostCode:</td> <td><input type="text" th:field="*{storePostcode}" /></td> </tr> <tr align="center"> <td><input type="submit" value="Submit" /></td> <td><input type="reset" value="Reset" /></td> </tr> </table> </form> </body> </html>
This html is rendered to allow the user to perform update operation on the entity. th:object="${store}"
is used to collect the form values into the model object.th:action="@{/updatestore}"
maps the POST request to the method storeUpdate()
of StoreManagementController
.
store.html
<!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Store Management</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> </head> <body> <h1>Store Management</h1> <form action="#" th:action="@{/addstore}" th:object="${store}" method="post"> <table> <tr> <td>Store Name:</td> <td><input type="text" th:field="*{storeName}" th:class="${#fields.hasErrors('storeName')}? fieldError" /></td> </tr> <tr> <td>Store Street Address :</td> <td><input type="text" th:field="*{storeStreetAddress}" /></td> </tr> <tr> <td>Store Suburb:</td> <td><input type="text" th:field="*{storeSuburb}" /></td> </tr> <tr> <td>Store PostCode:</td> <td><input type="text" th:field="*{storePostcode}" /></td> </tr> <tr align="center"> <td><input type="submit" value="Submit" /></td> <td><input type="reset" value="Reset" /></td> </tr> </table> </form> </body> </html>
th:action="@{/addstore}"
maps the POST request to the method storeAdd()
of StoreManagementController
.
storelist.html
<!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Store Details</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> </head> <body> <h1>Store Details</h1> <table> <tr> <th>ID</th> <th>NAME</th> <th>STREET ADDRESS</th> <th>SUBURB</th> <th>POSTCODE</th> </tr> <tr th:each="store : ${stores}"> <td th:text="${store.storeId}"></td> <td th:text="${store.storeName}"></td> <td th:text="${store.storeStreetAddress}"></td> <td th:text="${store.storeSuburb}"></td> <td th:text="${store.storePostcode}"></td> <td><a th:href="@{'/editstore/' + ${store.storeId}}">Edit</a></td> <td><a th:href="@{'/deletestore/' + ${store.storeId}}">Delete</a></td> </tr> <tr> <td colspan="2"> <p> <a href="/loadstore">Add another store?</a> </p> </td> </tr> </table> </body> </html>
This is to retrieve the list of entities and display on to the view. th:each="store : ${stores}
loops through the list of entities and renders them to the view.
3.4 Unit Test
AbstractUnitTest.java
package management.store; public abstract class AbstractUnitTest { }
The base class extended by all the unit test classes in our example.
AbstractControllerUnitTest.java
package management.store; import management.store.controller.BaseController; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @WebAppConfiguration public abstract class AbstractControllerUnitTest extends AbstractUnitTest { protected MockMvc mockMvc; protected void setUp(BaseController controller) { mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); } }
This is base class for all Controller unit test classes in our example.
StoreContollerMocksTest.java
package management.store.controller; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.ArrayList; import java.util.List; import management.store.AbstractControllerUnitTest; import management.store.model.Store; import management.store.repo.StoreRepository; import management.store.service.StoreManagementService; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; public class StoreContollerMocksTest extends AbstractControllerUnitTest { @Mock StoreManagementService storeService; @Mock StoreRepository storeRepo; @InjectMocks StoreManagementController storeController; @Before public void setUp() { MockitoAnnotations.initMocks(this); setUp(storeController); } //To stub data for service method. private List stubDataGetAllStores() { List stores = new ArrayList(); for (int i = 1; i < 3; i++) { Store st = new Store(); st.setStoreName("StubStore" + i); stores.add(st); } return stores; } @Test public void testGetAllStores() throws Exception { when(storeService.getAllStores()).thenReturn(stubDataGetAllStores()); String uri = "/getallstores"; MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get(uri)) .andReturn(); int status = result.getResponse().getStatus(); System.out.println("Status is :" + status); verify(storeService, times(1)).getAllStores(); Assert.assertTrue(status == 200); } }
@Mock
is used for creation of mocks for the service and repository beans. @InjectMocks
is used to inject the created mocks into the controller. when(storeService.getAllStores()).thenReturn(stubDataGetAllStores());
is to stub the method getAllStores()
to return a list of entities. This is a very simple example of using Mockito to write the unit tests.
3.5 Integration Test
AbstractIntegrationTest.java
package management.store; import management.StoreManagementApplication; import org.junit.runner.RunWith; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(StoreManagementApplication.class) @ActiveProfiles("test") public abstract class AbstractIntegrationTest { }
This is the base class for all the integration tests written in this tutorial. @RunWith(SpringJUnit4ClassRunner.class)
indicates that the class should use Spring’s JUnit facilities. @SpringApplicationConfiguration
provides an alternative to @ContextConfiguration
to configure the ApplicationContext
used in tests. @ActiveProfiles("test")
is to declare a Spring profile “test” for integration tests. The integration tests, when run with set @ActiveProfles
, will look for corresponding application.properties.
application-test.properties
spring.datasource.platform=mysql-test
In our example as the Spring profile is declared as “test”, the integration test looks for application-test.properties
.
As per the setting spring.datasource.platform=mysql-test
in the application-test.properties
, the corresponding schema-mysql-test.sql is exeuted.
schema-mysql-test.sql
CREATE TABLE IF NOT EXISTS store ( store_id bigint(20) NOT NULL AUTO_INCREMENT, store_name varchar(255) DEFAULT NULL, store_postcode varchar(255) DEFAULT NULL, store_street_address varchar(255) DEFAULT NULL, store_suburb varchar(255) DEFAULT NULL, PRIMARY KEY (store_id) ); INSERT IGNORE INTO store SET store_name= "S1",store_postcode= "1111",store_street_address="streetaddress1",store_suburb="suburb1"; INSERT IGNORE INTO store SET store_name= "S2",store_postcode= "2222",store_street_address="streetaddress2",store_suburb="suburb2"; INSERT IGNORE INTO store SET store_name= "S3",store_postcode= "3333",store_street_address="streetaddress3",store_suburb="suburb3"; INSERT IGNORE INTO store SET store_name= "S4",store_postcode= "4444",store_street_address="streetaddress4",store_suburb="suburb4"; INSERT IGNORE INTO store SET store_name= "S5",store_postcode= "5555",store_street_address="streetaddress5",store_suburb="suburb5"; INSERT IGNORE INTO store SET store_name= "S6",store_postcode= "6666",store_street_address="streetaddress6",store_suburb="suburb6";
Integrations tests when invoked, execute this sql script to create the table and insert the data.
AbstractControllerIntegrationTest.java
package management.store; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.IntegrationTest; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.context.WebApplicationContext; @WebAppConfiguration @IntegrationTest("server.port:0") @Transactional public abstract class AbstractControllerIntegrationTest extends AbstractIntegrationTest { protected MockMvc mockMvc; @Autowired protected WebApplicationContext webAppContext; protected void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext).build(); } }
@WebAppConfiguration
delcares that the ApplicationContext
loaded for the integration test should be a WebApplicationContext
. @IntegrationTest("server.port:0")
is to indicate that the test is an integration test and needs full startup like production application.
Do you know a convenient alternative for combination of @WebAppConfiguration
and @IntegrationTest
? You may use @WebIntegrationTest
to replace the combination of @WebAppConfiguration
and @IntegrationTest
. Go ahead and give a try using it.
@Transactional
here is used to rollback any transactions performed by the integration tests.
StoreControllerIntegrationTest.java
package management.store.controller; import management.store.AbstractControllerIntegrationTest; import management.store.service.StoreManagementService; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; public class StoreControllerIntegrationTest extends AbstractControllerIntegrationTest { @Autowired StoreManagementService storeManagementService; @Before public void setUp() { super.setUp(); } @Test public void testPlainLoadStore() throws Exception { String uri = "/loadstore"; MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get(uri)) .andReturn(); String content = result.getResponse().getContentAsString(); int status = result.getResponse().getStatus(); System.out.println("Status is :" + status); System.out.println("content is :" + content); Assert.assertTrue(status == 200); Assert.assertTrue(content.trim().length() > 0); } @Test public void testEditStore3() throws Exception { String uri = "/editstore/3"; MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get(uri)) .andExpect(MockMvcResultMatchers.view().name("editstore")) .andReturn(); String content = result.getResponse().getContentAsString(); int status = result.getResponse().getStatus(); System.out.println("Status is :" + status); System.out.println("content is :" + content); Assert.assertTrue(status == 200); Assert.assertTrue(content.trim().length() > 0); } @Test public void testDeleteStore3() throws Exception { String uri = "/deletestore/3"; MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get(uri)) .andReturn(); String content = result.getResponse().getContentAsString(); int status = result.getResponse().getStatus(); System.out.println("Status is :" + status); System.out.println("content is :" + content); Assert.assertTrue(status == 200); Assert.assertTrue(content.trim().length() > 0); } }
A method that is annotated with @Before
is executed before every test method in the test class. Spring MVC Test is built upon mock implementations of Servlet API that are avialable in spring-test
module. You may observe that, as @Transactional
is used, any database operations executed while executing the test methods, testDeleteStore3()
and testEditStore3()
will be rolled back once the test method exited.
4. Execute the tests
1. To run the unit and integration tests together use
gradlew clean build
or
gradlew clean build test integrationtest
2. To run only the unit tests use one of the commands as shown below
gradlew clean build test
or
gradlew clean build test -x integrationtest
3. To run only the integration tests use one of the commands as shown below
gradlew clean build integrationtest
or
gradlew clean build -x test integrationtest
The unit test reports and integration test reports can be found at:
${Project_folder}/build/reports/test/index.html ${Project_folder}/build/reports/integrationtest/index.html
5. Run the application
To run the application use one of the following commands
gradlew bootRun
or
gradlew build && java -jar build/libs/store_management-0.1.0.jar
The application can be accessed using http://localhost:8080/loadstore
.
6. References
- Spring Framework
- Spring Docs
- Spring Boot
- Gradle Documentation
- Gradle dependency management
- ThymeLeaf
- Mockito
- JUnit
- Spring Test Framework
- Integration Testing
7. Conclusion
In this tutorial we learnt how to use Spring Boot with help of a CRUD example.
You homework would be to further extend this example to use embedded database like h2 for integration testing, instead of MySQL as mentioned in this example. Hint: Spring profile configuration.
8. Download the Eclipse project
This was a Spring Boot Configuration Tutorial.
You can download the full source code of this example here: Spring Boot Configuration Example
Hi,
While building the application I am getting the following error. Please let me know what configuration i am missing.
management.store.controller.StoreControllerIntegrationTest > testPlainLoadStore FAILED
java.lang.IllegalStateException
Caused by: org.springframework.beans.factory.BeanCreationException
Caused by: org.springframework.beans.factory.BeanCreationException
Caused by: org.springframework.beans.factory.BeanCreationException
Caused by: org.springframework.beans.factory.BeanCreationException
Caused by: org.springframework.jdbc.datasource.init.UncategorizedScriptException
Caused by: org.springframework.jdbc.CannotGetJdbcConnectionException
Caused by: java.sql.SQLException