web java单元测试 实例_单元测试 - 探索java web 单元测试的正确姿势

单元测试 - 探索java web 单元测试的正确姿势

一丶起因

笔者一直听闻TDD,自动化测试等高大上的技术名词, 向往其中的便利之处, 但一直求而不得, 只因项目中有各种依赖的存在,其中最大的依赖便是数据库. java web 项目大部分都是写sql语句, 不依赖数据库, 便测试不了sql语句的正确性, 但依赖数据库又有种种不变之处. 除此之外, 还有种种类与类之间的依赖关系,很不方便. 遗憾的是, 网上各种文章参差不齐, 笔者所参与的项目很少甚至没有单元测试, 修改代码, 如履薄冰. 在苦思不得其解之际, 向优秀开源项目mybatis求取经验, 终获得一些答案.

二丶实践思路

mybatis使用单元测试的方式是使用内存数据库做单元测试,单元测试前,先根据配置以及数据库脚本,初始化内存数据库,然后再使用内存数据库测试.所以,笔者也是采用这种思路.

web java单元测试 实例_单元测试 - 探索java web 单元测试的正确姿势_第1张图片

除此之外, 还有各种类与类之间依赖关系, 笔者依据mybatis以及spring选择使用mockito框架mock解决

所以选用的工具有 hsql内存数据库, mockito mock工具, junit单元测试工具, spring-boot-test子项目

三丶实施测试

1. 在pom.xml添加hsql 以及mockito

org.hsqldb

hsqldb

2.5.0

test

org.mockito

mockito-core

3.1.0

test

2. 在test/resources/databases/jpetstore 下添加hsql 数据源的配置, 其中显示设置sql.syntax_mys=true 是对mysql的支持

driver=org.hsqldb.jdbcDriver

## 配置hsql最大程度兼容mysql

url=jdbc:hsqldb:.;sql.syntax_mys=trueusername=sa

password=

以及在该文件夹下配置初始化化mysql测试数据的脚本

3. 配置初始化内存测试库

BaseDataTest.java  来源于mybatis,用于运行sql脚本,初始化测试库

public abstract classBaseDataTest {public static final String BLOG_PROPERTIES = "org/apache/ibatis/databases/blog/blog-derby.properties";public static final String BLOG_DDL = "org/apache/ibatis/databases/blog/blog-derby-schema.sql";public static final String BLOG_DATA = "org/apache/ibatis/databases/blog/blog-derby-dataload.sql";public static final String JPETSTORE_PROPERTIES = "org/apache/ibatis/databases/jpetstore/jpetstore-hsqldb.properties";public static final String JPETSTORE_DDL = "org/apache/ibatis/databases/jpetstore/jpetstore-hsqldb-schema.sql";public static final String JPETSTORE_DATA = "org/apache/ibatis/databases/jpetstore/jpetstore-hsqldb-dataload.sql";public static UnpooledDataSource createUnpooledDataSource(String resource) throwsIOException {

Properties props=Resources.getResourceAsProperties(resource);

UnpooledDataSource ds= newUnpooledDataSource();

ds.setDriver(props.getProperty("driver"));

ds.setUrl(props.getProperty("url"));

ds.setUsername(props.getProperty("username"));

ds.setPassword(props.getProperty("password"));returnds;

}public static PooledDataSource createPooledDataSource(String resource) throwsIOException {

Properties props=Resources.getResourceAsProperties(resource);

PooledDataSource ds= newPooledDataSource();

ds.setDriver(props.getProperty("driver"));

ds.setUrl(props.getProperty("url"));

ds.setUsername(props.getProperty("username"));

ds.setPassword(props.getProperty("password"));returnds;

}public static void runScript(DataSource ds, String resource) throwsIOException, SQLException {try (Connection connection =ds.getConnection()) {

ScriptRunner runner= newScriptRunner(connection);

runner.setAutoCommit(true);

runner.setStopOnError(false);

runner.setLogWriter(null);

runner.setErrorLogWriter(null);

runScript(runner, resource);

}

}public static void runScript(ScriptRunner runner, String resource) throwsIOException, SQLException {try (Reader reader =Resources.getResourceAsReader(resource)) {

runner.runScript(reader);

}

}public static DataSource createBlogDataSource() throwsIOException, SQLException {

DataSource ds=createUnpooledDataSource(BLOG_PROPERTIES);

runScript(ds, BLOG_DDL);

runScript(ds, BLOG_DATA);returnds;

}public static DataSource createJPetstoreDataSource() throwsIOException, SQLException {

DataSource ds=createUnpooledDataSource(JPETSTORE_PROPERTIES);

runScript(ds, JPETSTORE_DDL);

runScript(ds, JPETSTORE_DATA);returnds;

}

}

配置数据源, 初始化数据库, 以及配置生成mapper

/*** 准备内存数据库中的数据

*@authorTimFruit

* @date 19-11-17 上午10:50*/@Configurationpublic classBaseDataConfig {public static final Logger logger=LoggerFactory.getLogger(BaseDataConfig.class);

String dataPrefix= "databases/jpetstore/";//数据源

@Bean("myDataSource")publicDataSource createDataSource() {//创建数据源

logger.info("创建数据源...");

InputStream inputStream=FileUtil.getInputStream(dataPrefix+"jpetstore-hsql2mysql.properties");

Properties properties=newProperties();try{

properties.load(inputStream);

}catch(IOException e) {throw newRuntimeException(e);

}

HikariDataSource dataSource=newHikariDataSource();

dataSource.setDriverClassName(properties.getProperty("driver"));

dataSource.setJdbcUrl(properties.getProperty("url"));

dataSource.setUsername(properties.getProperty("username"));

dataSource.setPassword(properties.getProperty("password"));//准备数据

logger.info("准备数据...");try{

BaseDataTest.runScript(dataSource, dataPrefix+"jpetstore-mysql-schema.sql");

BaseDataTest.runScript(dataSource, dataPrefix+"jpetstore-mysql-dataload.sql");

}catch(Exception e) {throw newRuntimeException(e);

}

logger.info("准备数据完成...");returndataSource;

}//mapper

@BeanpublicSqlSessionFactory createSqlSessionFactoryBean (

@Qualifier("myDataSource") DataSource dataSource,

@Autowired MybatisProperties mybatisProperties)throwsException {

SqlSessionFactoryBean factory= newSqlSessionFactoryBean();

factory.setDataSource(dataSource);if (!ObjectUtils.isEmpty(mybatisProperties.resolveMapperLocations())) {

factory.setMapperLocations(mybatisProperties.resolveMapperLocations());

}returnfactory.getObject();

}

}

4. spring-boot-test对mockito的支持

使用@SpyBean和@MockBean修饰属性, spring会对该类型的bean进行spy或者mock操作, 之后装配该类型属性的时候, 都会使用spy或者mock之后的bean进行装配.  关于Mockito的使用可以看我前一篇文章

//@MockBean

@SpyBean

AccountMapper accountMapper;

@SpyBean

AccountService accountService;

@Testpublic voidshouldSelectAccount(){

Account mockAccount=createTimFruitAccount();//打桩

doReturn(mockAccount)

.when(accountMapper)

.selectAccount(timfruitUserId);//测试service方法

Account result=accountService.selectAccount(timfruitUserId);//验证

Assert.assertEquals(mockAccount, result);

}

需要注意的时, 使用@SpyBean修饰Mapper类的时候, 需要设置mockito对final类型的支持, 否则会报"Mockito cannot mock/spy because : - final class"的异常

设置方式如下:

先在resources文件夹下,新建mockito-extensions文件夹,在该文件夹下新建名为org.mockito.plugins.MockMaker的文本文件,添加以下内容:

mock-maker-inline

5. 对Mapper sql 进行单元测试

避免各单元测试方法的相互影响, 主要利用数据库的事务, 测试完之后, 回滚事务, 不应影响其他测试

@RunWith(SpringRunner.class)

@SpringBootTest//加事务, 在单元测试中默认回滚测试数据, 各个测试方法互不影响// https://blog.csdn.net/qq_36781505/article/details/85339640

@Transactionalpublic classAccountMapperTests {

@SpyBean

AccountMapper accountMapper;

@Testpublic voidshouldSelectAccount(){//given 测试数据库中的数据//when

Account result=accountMapper.selectAccount("ttx");//then

Assert.assertEquals("[email protected]", result.getEmail());

}

@Testpublic voidshouldInsertAccount(){//given

Account timAccount=createTimFruitAccount();//when

accountMapper.insertAccount(timAccount);//then

Account result=accountMapper.selectAccount(timfruitUserId);

Assert.assertEquals(timfruitUserId, result.getUserid());

Assert.assertEquals(timAccount.getCity(), result.getCity());

Assert.assertEquals(timAccount.getAddr1(), result.getAddr1());

Assert.assertEquals(timAccount.getAddr2(), result.getAddr2());

}

@Testpublic voidshouldUpdateCountry(){//given

String userId="ttx";

String country="万兽之国";//when

accountMapper.updateCountryByUserId(userId, country);//then

Account result=accountMapper.selectAccount(userId);

Assert.assertEquals(country, result.getCountry());

}

@Testpublic voidshouldDeleteAccount(){//given

String userId="ttx";

Account account=accountMapper.selectAccount(userId);

Assert.assertTrue(account!=null);//when

accountMapper.deleteAccountry(userId);//then

account=accountMapper.selectAccount(userId);

Assert.assertTrue(account==null);

}

@Testpublic voidshouldSaveAccountBatch(){//given//update

String userId1="ttx";

String country1="天堂";

Account account1=accountMapper.selectAccount(userId1);

account1.setCountry(country1);//insert

String userId2=timfruitUserId;

String country2="中国";

Account account2=createTimFruitAccount();

account2.setCountry(country2);

List accountList=Arrays.asList(account1,account2);//when

accountMapper.saveAccountBatch(accountList);//then

account1=accountMapper.selectAccount(userId1);

Assert.assertEquals(country1, account1.getCountry());

account2=accountMapper.selectAccount(userId2);

Assert.assertEquals(country2, account2.getCountry());

}

}

四丶后记

1. 这里仅仅只是对java web 单元测试实践提供一种思路, 笔者尚未在真实项目中应用, 有可能存在一些坑, 如hsql对mysql的兼容性支持等.任重而道远

2. 单元测试要想实施方便, 主要思路是除去各种依赖

3. 多向优秀开源项目学习, 我所走之路, 前人早已开辟, 这其实也是看源码的一个好处

补充(2020-04-25):

使用内存数据库兼容性不好,流程不变,将内存数据库改为docker运行数据库更为可行

你可能感兴趣的:(web,java单元测试,实例)