如果打算做一些现有代码的重构,强烈建议重构要有UT保障,毕竟,没有UT的重构就是裸奔…
{待测试类名} + “Test”后缀
例如:
CustomerSvcMgrServiceTest
“test” + {被测试的方法名称} + “_Should” + {预期结果} + “_When” + {条件}
例如:
testXXX_ShouldSuccess_WhenDoingSth
testXXX_ShouldSuccess
如果ut方法名无法取得较好的自注释效果,可对ut方法增加注释说明。
@Mock是Mockito提供的注解,用于创建待测类的模拟对象。
@MockBean不仅是创建待测类的模拟对象,还将这个模拟对象加到spring上下文(ApplicationContext)里。如果我们使用的是SpringBootTest做集成测试,且要mock一个已有的bean(@Service、@Component等注解标注),应该使用@MockBean,而不是@Mock。
@MockBean一般用于成员级别,还有一个@MockBeans的注解,最终效果跟@MockBean差不多,必须放到类级别,然后在成员字段引入的时候依然用@Autowired或@Inject,比如下例:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = StartupApplication.class)
@Slf4j
@MockBeans({@MockBean(IPhaseService.class)})
public class PhaseCacheHelperTest {@Autowiredprivate IPhaseService phaseService;@Testpublic void testGetByPhaseCode_ShouldLoadFromDs() {...Mockito.when(phaseService.findPhase(phaseCode)).thenReturn(resp);PhaseVO actualVo = PhaseCacheHelper.getByPhaseCode(phaseCode);Assert.assertNotNull(actualVo);Assert.assertEquals(actualVo.getPhaseName(), expectVo.getPhaseName());Mockito.verify(phaseService, Mockito.times(1)).findPhase(phaseCode);}
用MockBeans注解的时候,测试框架会提前把IPhaseService类mock化,再用通常的 @Autowired注解注入,它不会像@Mockbean那样走MockitoPostProcessor的后处理过程。这个后处理过程在有些情况下会产生多次注入的错误:
the field private cannot have an existing value
关于Mockito的参数匹配,参看:
https://blog.csdn.net/listeningsea/article/details/123224131
我们用的比较多的是精确匹配和模糊匹配。精确匹配要注意,如果传入的是一个自定义对象的话,要求这个对象有equals方法,不然做不到精确匹配。
@SpringBootTest为我们的测试提供了spring boot支持,它启动了spring上下文,提供的是E2E的测试能力,所以本质上它已经属于IT的范围了。我们将IT放到UT阶段做,提前发现问题,并无不妥,不用拘泥于概念。
说明一下,@RunWith(SpringRunner.class)是junit4的写法,junit5(jupyter)下已经没有这个注解了。
我理解,如果将@SpringBootTest认为是IT的话,DB可以不mock,连接真实环境能提前发现一些很明显的sql语法错误,一个典型的场景是为mybatis mapper接口写测试。当然,如果测试的重点是代码逻辑,或者要构造一些DB异常,那DB还是要mock的。
void函数既然无法检查返回值,可以视情况检查副作用,比如一些潜在的数据变化、调用次数。
既然DB不mock,那就得连真实数据库,可能的做法是:要么本地搭一个mysql,要么连接远程mysql。前者有搭建工作量,后者受制于环境数据,跑UT容易出问题。
因此,考虑在UT中引入H2内存数据库,外加flyway来初始化数据库表结构。
我们先做一个bean来在dataSource就绪时立刻执行建表语句:
@Component
@Slf4j
@DependsOnDatabaseInitialization
public class DBPrepare {private static final String H2SQL_DIR = "/tmp/H2SQL";@Injectprivate DataSource dataSource;@PostConstructpublic void init() {// only H2 db use flywayif (isH2DB()) {log.info("DBPrepare");genH2SqlScript();String[] locations = new String[] {"filesystem:" + H2SQL_DIR + "/"};Flyway flyway = buildFlyway(dataSource, "prjname", locations);try {flyway.migrate();log.info("DBPrepare success");} finally {cleanH2SqlScript();}}}...
}
由于原始的mysql脚本在H2里执行要做一些调整,所以我们写了genH2SqlScript来做这个转换的事情。
转换的逻辑看下节。
接着,要看H2与mysql的兼容性,幸运的是H2可以设置MYSQL模式:
MODE=MySQL
以兼容mysql建表语句。不过,即使在兼容mysql的模式下,也有些不同之处需注意:
我们设置的H2连接字符串为:
jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=MySQL;DATABASE_TO_LOWER=TRUE
选项说明如下:
By default, closing the last connection to a database closes the database. For an in-memory database, this means the content is lost. To keep the database open, add DB_CLOSE_DELAY=-1 to the database URL. To keep the content of an in-memory database as long as the virtual machine is alive, use jdbc:h2:mem:test;DB_CLOSE_DELAY=-1By default, a database is closed when the last connection is closed. However, if it is never closed, the database is closed when the virtual machine exits normally, using a shutdown hook. In some situations, the database should not be closed in this case, for example because the database is still used at virtual machine shutdown (to store the shutdown process in the database for example). For those cases, the automatic closing of the database can be disabled in the database URL. The first connection (the one that is opening the database) needs to set the option in the database URL (it is not possible to change the setting afterwards). The database URL to disable database closing on exit is:
String url = "jdbc:h2:~/test;DB_CLOSE_ON_EXIT=FALSE";
参考:http://www.h2database.com/html/features.html