After I introduced embedded mysql for unit testing to my project, the build is taking longer now. For my local development, sometimes I want to disable the unit test:
mvn clean package -DskipTests
Note that you can even skip the compiling of test class by using
mvn clean package -Dmaven.test.skip
However, if you use cobertura for code coverage check, you would get the following error:
[ERROR] Project failed check. Total branch coverage rate of 0.0% is below 85.0%
Project failed check. Total line coverage rate of 0.0% is below 85.0%
So you need to skip cobertura as well. if you google "skip cobertura", most answers suggested using haltOnFailure. However, cobertura already provides the skip functionality:
mvn clean package -DskipTests -Dcobertura.skip
With -Dcobertura.skip, cobertura check will be skipped:
[INFO] --- cobertura-maven-plugin:2.5.2:instrument (default) @ tag-service ---
[INFO] Skipping cobertura execution
Search This Blog
Tuesday, June 11, 2013
Monday, June 10, 2013
Using embedded mysql database for unit test with maven and spring
I used H2 in memory database to unit test my DAO. However, my production database is MySQL.
Generally, H2 is compatible with MySQL. However, H2 doesn't support ENUM type yet, and my schema uses ENUM for a column definition. Inspired by this post and another, I successfully used embedded MySQL for unit test. Here is what I did:
1. Add mysql-connector-mxj dependency:
2. Create EmbeddedMysqlDatabase.java and EmbeddedMysqlDatabaseBuilder.java
3. Create spring bean datasource:
4. Run unit test with Spring:
A couple things are worth mentioning:
1. The test uses Spring annotation @DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) which reload the context after every test method. It means every test method starts a new mysql instance. You may group test methods to 2 different categories, one changes data, and another not. For those methods don't change data, only need to create one instance of mysql to speed up testing.
2. If you see the following error message on Linux server:
/lib64/libc.so.6: version `GLIBC_2.7' not found (required by /tmp/test_db_2420035739165052/bin/mysqld)
You need to either downgrade mysql-connector-mxj to 5.0.11 or upgrade your linux OS. I got the above error message on CentOS 5.4 with 5.0.12, but no problem with 5.0.11. However, 5.0.11 isn't in any public maven repo, but you can download it from here and install it to your local repo, or upload to your company's repo. Using the following command to check CentOS version:
cat /etc/redhat-release
3. The code is tested on Mac and CentOS only, and can be downloaded from github
Generally, H2 is compatible with MySQL. However, H2 doesn't support ENUM type yet, and my schema uses ENUM for a column definition. Inspired by this post and another, I successfully used embedded MySQL for unit test. Here is what I did:
1. Add mysql-connector-mxj dependency:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<dependency> | |
<groupId>mysql</groupId> | |
<artifactId>mysql-connector-mxj</artifactId> | |
<version>5.0.12</version> | |
<scope>test</scope> | |
</dependency> |
2. Create EmbeddedMysqlDatabase.java and EmbeddedMysqlDatabaseBuilder.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.io.IOException; | |
import org.apache.commons.io.FileUtils; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
import org.springframework.jdbc.datasource.DriverManagerDataSource; | |
import com.mysql.management.MysqldResource; | |
public class EmbeddedMysqlDatabase extends DriverManagerDataSource { | |
private final Logger logger = LoggerFactory.getLogger(EmbeddedMysqlDatabase.class); | |
private final MysqldResource mysqldResource; | |
public EmbeddedMysqlDatabase(MysqldResource mysqldResource) { | |
this.mysqldResource = mysqldResource; | |
} | |
public void shutdown() { | |
if (mysqldResource != null) { | |
mysqldResource.shutdown(); | |
if (!mysqldResource.isRunning()) { | |
logger.info(">>>>>>>>>> DELETING MYSQL BASE DIR [{}] <<<<<<<<<<", mysqldResource.getBaseDir()); | |
try { | |
FileUtils.forceDelete(mysqldResource.getBaseDir()); | |
} catch (IOException e) { | |
logger.error(e.getMessage(), e); | |
} | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.io.File; | |
import java.util.HashMap; | |
import java.util.Map; | |
import java.util.Random; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
import org.springframework.core.io.DefaultResourceLoader; | |
import org.springframework.core.io.ResourceLoader; | |
import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils; | |
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; | |
import com.mysql.management.MysqldResource; | |
import com.mysql.management.MysqldResourceI; | |
public class EmbeddedMysqlDatabaseBuilder { | |
private static final Logger LOG = LoggerFactory.getLogger(EmbeddedMysqlDatabaseBuilder.class); | |
private final String baseDatabaseDir = System.getProperty("java.io.tmpdir"); | |
private String databaseName = "test_db_" + System.nanoTime(); | |
private final int port = new Random().nextInt(10000) + 3306; | |
private final String username = "root"; | |
private final String password = ""; | |
private boolean foreignKeyCheck; | |
private final ResourceLoader resourceLoader; | |
private final ResourceDatabasePopulator databasePopulator; | |
public EmbeddedMysqlDatabaseBuilder() { | |
resourceLoader = new DefaultResourceLoader(); | |
databasePopulator = new ResourceDatabasePopulator(); | |
foreignKeyCheck = true; | |
} | |
private EmbeddedMysqlDatabase createDatabase(MysqldResource mysqldResource) { | |
if (!mysqldResource.isRunning()) { | |
LOG.error("MySQL instance not found... Terminating"); | |
throw new RuntimeException("Cannot get Datasource, MySQL instance not started."); | |
} | |
EmbeddedMysqlDatabase database = new EmbeddedMysqlDatabase(mysqldResource); | |
database.setDriverClassName("com.mysql.jdbc.Driver"); | |
database.setUsername(username); | |
database.setPassword(password); | |
String url = "jdbc:mysql://localhost:" + port + "/" + databaseName + "?" + "createDatabaseIfNotExist=true"; | |
if (!foreignKeyCheck) { | |
url += "&sessionVariables=FOREIGN_KEY_CHECKS=0"; | |
} | |
LOG.debug("database url: {}", url); | |
database.setUrl(url); | |
return database; | |
} | |
private MysqldResource createMysqldResource() { | |
if (LOG.isDebugEnabled()) { | |
LOG.debug("=============== Starting Embedded MySQL using these parameters ==============="); | |
LOG.debug("baseDatabaseDir : " + baseDatabaseDir); | |
LOG.debug("databaseName : " + databaseName); | |
LOG.debug("host : localhost (hardcoded)"); | |
LOG.debug("port : " + port); | |
LOG.debug("username : root (hardcode)"); | |
LOG.debug("password : (no password)"); | |
LOG.debug("============================================================================="); | |
} | |
Map<String, String> databaseOptions = new HashMap<String, String>(); | |
databaseOptions.put(MysqldResourceI.PORT, Integer.toString(port)); | |
MysqldResource mysqldResource = new MysqldResource(new File(baseDatabaseDir, databaseName)); | |
mysqldResource.start("embedded-mysqld-thread-" + System.currentTimeMillis(), databaseOptions); | |
if (!mysqldResource.isRunning()) { | |
throw new RuntimeException("MySQL did not start."); | |
} | |
LOG.info("MySQL started successfully @ {}", System.currentTimeMillis()); | |
return mysqldResource; | |
} | |
private void populateScripts(EmbeddedMysqlDatabase database) { | |
try { | |
DatabasePopulatorUtils.execute(databasePopulator, database); | |
} catch (Exception e) { | |
LOG.error(e.getMessage(), e); | |
database.shutdown(); | |
} | |
} | |
public EmbeddedMysqlDatabaseBuilder addSqlScript(String script) { | |
databasePopulator.addScript(resourceLoader.getResource(script)); | |
return this; | |
} | |
/** | |
* whether to enable mysql foreign key check | |
* | |
* @param foreignKeyCheck | |
*/ | |
public EmbeddedMysqlDatabaseBuilder setForeignKeyCheck(boolean foreignKeyCheck) { | |
this.foreignKeyCheck = foreignKeyCheck; | |
return this; | |
} | |
/** | |
* @param databaseName | |
* the databaseName to set | |
*/ | |
public final void setDatabaseName(String databaseName) { | |
this.databaseName = databaseName; | |
} | |
public EmbeddedMysqlDatabase build() { | |
MysqldResource mysqldResource = createMysqldResource(); | |
EmbeddedMysqlDatabase database = createDatabase(mysqldResource); | |
populateScripts(database); | |
return database; | |
} | |
} |
3. Create spring bean datasource:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import javax.sql.DataSource; | |
import org.springframework.context.annotation.Bean; | |
import org.springframework.context.annotation.Configuration; | |
@Configuration | |
public class EmbeddedDataSourceConfig implements DataSourceConfig { | |
@Override | |
@Bean(destroyMethod="shutdown") | |
public DataSource dataSource() { | |
return new EmbeddedMysqlDatabaseBuilder().addSqlScript("tag_schema.sql").addSqlScript("tag_init.sql").build(); | |
} | |
} |
4. Run unit test with Spring:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import static org.hamcrest.CoreMatchers.is; | |
import static org.junit.Assert.assertNull; | |
import static org.junit.Assert.assertThat; | |
import javax.sql.DataSource; | |
import org.junit.Before; | |
import org.junit.Test; | |
import org.junit.runner.RunWith; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.dao.EmptyResultDataAccessException; | |
import org.springframework.jdbc.core.simple.SimpleJdbcInsert; | |
import org.springframework.test.annotation.DirtiesContext; | |
import org.springframework.test.annotation.DirtiesContext.ClassMode; | |
import org.springframework.test.context.ContextConfiguration; | |
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; | |
import com.zhentao.embedded.mysql.config.EmbeddedDataSourceConfig; | |
import com.zhentao.embedded.mysql.dao.TagDaoJdbcImpl; | |
import com.zhentao.embedded.mysql.model.Tag; | |
@RunWith(SpringJUnit4ClassRunner.class) | |
@ContextConfiguration(classes=EmbeddedDataSourceConfig.class) | |
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) | |
public class TagDaoTest { | |
@Autowired | |
private DataSource dataSource; | |
private TagDaoJdbcImpl tagDao; | |
@Before | |
public void setUp() { | |
tagDao = new TagDaoJdbcImpl(); | |
tagDao.setDataSource(dataSource); | |
tagDao.setJdbcInsert(new SimpleJdbcInsert(dataSource).withTableName("tag").usingGeneratedKeyColumns("id")); | |
} | |
@Test(expected = EmptyResultDataAccessException.class) | |
public void testFindByIdNotExists() { | |
tagDao.findById(10000); | |
} | |
@Test | |
public void testInsert() { | |
Tag tag = new Tag(3, 1, "lift", "source"); | |
tag = tagDao.insert(tag); | |
Tag actual = tagDao.findById(tag.getId()); | |
assertThat(actual, is(tag)); | |
} | |
@Test | |
public void testUpdate() { | |
String adSource = "2"; | |
Tag tag = new Tag(2, 2, "lift", adSource); | |
tagDao.update(tag); | |
Tag actual = tagDao.findByKey(tag.getTagId(), tag.getAdvertiserAccountId(), tag.getType()); | |
assertThat(actual.getAdSource(), is(adSource)); | |
} | |
@Test | |
public void testFindByKeyReturnNull() { | |
assertNull(tagDao.findByKey(100l, 100l, "not exist")); | |
} | |
} |
A couple things are worth mentioning:
1. The test uses Spring annotation @DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) which reload the context after every test method. It means every test method starts a new mysql instance. You may group test methods to 2 different categories, one changes data, and another not. For those methods don't change data, only need to create one instance of mysql to speed up testing.
2. If you see the following error message on Linux server:
/lib64/libc.so.6: version `GLIBC_2.7' not found (required by /tmp/test_db_2420035739165052/bin/mysqld)
You need to either downgrade mysql-connector-mxj to 5.0.11 or upgrade your linux OS. I got the above error message on CentOS 5.4 with 5.0.12, but no problem with 5.0.11. However, 5.0.11 isn't in any public maven repo, but you can download it from here and install it to your local repo, or upload to your company's repo. Using the following command to check CentOS version:
cat /etc/redhat-release
3. The code is tested on Mac and CentOS only, and can be downloaded from github
Labels:
DAO test mysql,
embedded mysql,
maven,
mxj,
mysql,
Spring,
unit test
Monday, June 3, 2013
Example for enabling CORS support in Spring Rest Api 3.2
My last year's post "Enable CORS support in REST services with Spring 3.1" seems causing some confusion. I decided to create an example to show how to enable CORS with Spring rest api. The CorsFilter is same as before:
Below are 2 endpoints from EmployeeController.java:
The update method adds the header Access-Control-Allow-Origin with "*", but delete method doesn't. Therefore, the update method is enabled with CORS, but delete isn't. If delete endpoint is called, the following error will be shown in Chrome:
"cannot load http://localhost:8080/rest/employee/1. Origin http://127.0.0.1:8080 is not allowed by Access-Control-Allow-Origin."
However, the delete method is still invoked on the server side since the pre-flight request (OPTIONS)
allows DELETE method to be called.
The entire project can be downloaded from github. Following README to test it.
My intention was to disable/enable CORS support in each individual method by setting "Access-Control-Allow-Origin", but it seems not working as expected: Although the browser returns correct info, the method call is still invoked on the server side even Access-Control-Allow-Origin is not set. If you are allowed to enable all endpoints with CORS support, the code can be simplified as below:
The only difference is that addHeader("Access-Control-Allow-Origin") is moved out the if check. And then the update method can be simplified as:
The code can be downloaded from github too.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class CorsFilter extends OncePerRequestFilter { | |
@Override | |
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) | |
throws ServletException, IOException { | |
if (request.getHeader("Access-Control-Request-Method") != null && "OPTIONS".equals(request.getMethod())) { | |
// CORS "pre-flight" request | |
response.addHeader("Access-Control-Allow-Origin", "*"); | |
response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE"); | |
response.addHeader("Access-Control-Allow-Headers", "Content-Type"); | |
response.addHeader("Access-Control-Max-Age", "1");// 30 min | |
} | |
filterChain.doFilter(request, response); | |
} | |
} |
Below are 2 endpoints from EmployeeController.java:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Controller | |
@RequestMapping("/employee") | |
public class EmployeeController { | |
private static ConcurrentHashMap<Long, Employee> employees = new ConcurrentHashMap<Long, Employee>(); | |
@RequestMapping(value = "/{id}", method = RequestMethod.PUT, consumes = { "application/json" }) | |
public ResponseEntity<Employee> update(@RequestBody Employee employee, @PathVariable long id) { | |
if (employee.getId() != id) { | |
throw new IdNotMatchException(); | |
} | |
employees.put(id, employee); | |
HttpHeaders headers = addAccessControllAllowOrigin(); | |
ResponseEntity<Employee> entity = new ResponseEntity<Employee>(headers, HttpStatus.OK); | |
return entity; | |
} | |
private HttpHeaders addAccessControllAllowOrigin() { | |
HttpHeaders headers = new HttpHeaders(); | |
headers.add("Access-Control-Allow-Origin", "*"); | |
return headers; | |
} | |
/** | |
* CORS isn't enabled for delete method since no Access-Control-Allow-Origin is set in header | |
* | |
* @param id | |
*/ | |
@ResponseStatus(value = HttpStatus.NO_CONTENT) | |
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE) | |
public void delete(@PathVariable long id) { | |
if (!employees.containsKey(id)) { | |
throw new EmployeeNotExistsException(); | |
} | |
employees.remove(id); | |
} | |
//other methods go here | |
} |
The update method adds the header Access-Control-Allow-Origin with "*", but delete method doesn't. Therefore, the update method is enabled with CORS, but delete isn't. If delete endpoint is called, the following error will be shown in Chrome:
"cannot load http://localhost:8080/rest/employee/1. Origin http://127.0.0.1:8080 is not allowed by Access-Control-Allow-Origin."
However, the delete method is still invoked on the server side since the pre-flight request (OPTIONS)
allows DELETE method to be called.
The entire project can be downloaded from github. Following README to test it.
My intention was to disable/enable CORS support in each individual method by setting "Access-Control-Allow-Origin", but it seems not working as expected: Although the browser returns correct info, the method call is still invoked on the server side even Access-Control-Allow-Origin is not set. If you are allowed to enable all endpoints with CORS support, the code can be simplified as below:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class CorsFilter extends OncePerRequestFilter { | |
@Override | |
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) | |
throws ServletException, IOException { | |
response.addHeader("Access-Control-Allow-Origin", "*"); | |
if (request.getHeader("Access-Control-Request-Method") != null && "OPTIONS".equals(request.getMethod())) { | |
// CORS "pre-flight" request | |
response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE"); | |
response.addHeader("Access-Control-Allow-Headers", "Content-Type"); | |
response.addHeader("Access-Control-Max-Age", "1");// 30 min | |
} | |
filterChain.doFilter(request, response); | |
} | |
} |
The only difference is that addHeader("Access-Control-Allow-Origin") is moved out the if check. And then the update method can be simplified as:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@RequestMapping(value = "/{id}", method = RequestMethod.PUT, consumes = { "application/json" }) | |
@ResponseStatus(HttpStatus.OK) | |
public void update(@RequestBody Employee employee, @PathVariable long id) { | |
if (employee.getId() != id) { | |
throw new IdNotMatchException(); | |
} | |
employees.put(id, employee); | |
} |
The code can be downloaded from github too.
Labels:
CORS,
CORS Spring,
CORS Spring Rest,
jQuery,
rest api,
Spring,
Spring rest
Subscribe to:
Posts (Atom)