一个可维护的可读的单元测试对于提升软件质量,减少bug数等是至关重要的。单元测试有以下几个好处: 1. 当修改代码逻辑,重构,升级版本或增加新feature时不会引入bug,不会引起故障 2. 可以通过读单元测试来理解代码逻辑 以下是unit test的一些要点 • 要尽量短和易于理解 • 每一个ut要只测试一个代码逻辑 • unit test要自包含,即不要使用外部变量,外部数据库的数据,外部链接等 • ut不要使用随机变量,System.currentSystemMillis(), Instance.now()这样的变量值 • 尽量避免使用Assert.assertTrue或者Assert.assertFalse

将ut规格化

1. 初始化:给变量赋值,设置mock等 2. 调用:调用你想测的函数 3. Assert:Assert输出是否符合预期 这三部分最好用空行分开
  1. ProductDTO product1 = requestProduct(1);
  2. ProductDTO product2 = new ProductDTO("1", List.of(State.ACTIVE, State.REJECTED))
  3. assertThat(product1).isEqualTo(product2);

变量名要清晰

像下面这段代码的变量名就不好理解 最好是改成
  1. ProductDTO actualProduct = requestProduct(1);
  2. ProductDTO expectedProduct = new ProductDTO("1", List.of(State.ACTIVE, State.REJECTED))
  3. assertThat(actualProduct).isEqualTo(expectedProduct);
这样就可以知道第一个变量是实际得到的变量值,而第二个变量是预期的变量值

ut要短

ut太长会让人看不懂。通过大量使用helper function来达到缩短ut的长度,比如:
  1. @Test
  2. public void categoryQueryParameter() throws Exception {
  3. List products = List.of(
  4. new ProductEntity().setId("1").setName("Envelope").setCategory("Office").setDescription("An Envelope").setStockAmount(1),
  5. new ProductEntity().setId("2").setName("Pen").setCategory("Office").setDescription("A Pen").setStockAmount(1),
  6. new ProductEntity().setId("3").setName("Notebook").setCategory("Hardware").setDescription("A Notebook").setStockAmount(2)
  7. );
  8. for (ProductEntity product : products) {
  9. template.execute(createSqlInsertStatement(product));
  10. }
  11. String responseJson = client.perform(get("/products?category=Office"))
  12. .andExpect(status().is(200))
  13. .andReturn().getResponse().getContentAsString();
  14. assertThat(toDTOs(responseJson))
  15. .extracting(ProductDTO::getId)
  16. .containsOnly("1", "2");
  17. }
通过将其中的初始化代码包装到helper function中来缩短ut长度
  1. @Test
  2. public void categoryQueryParameter() throws Exception {
  3. insertIntoDatabase(
  4. createProductWithCategory("1", "Office"),
  5. createProductWithCategory("2", "Office"),
  6. createProductWithCategory("3", "Hardware")
  7. );
  8. String responseJson = requestProductsByCategory("Office");
  9. assertThat(toDTOs(responseJson))
  10. .extracting(ProductDTO::getId)
  11. .containsOnly("1", "2");
  12. }

一个ut只测试一个逻辑

ut只能测试一个逻辑,多个逻辑需要添加多个ut来进行测试,比如下面这样就不行
  1. public class ProductControllerTest {
  2. @Test
  3. public void testProduct() {
  4. // a lot of codes here...
  5. }
  6. }
分割成多个ut来测试各个corner case
  1. public class ProductControllerTest {
  2. @Test
  3. public void testSingleProduct() {
  4. }
  5. @Test
  6. public void testMultpleProduct() {
  7. }
  8. @Test
  9. public void testProductWithNullParams() {
  10. }
  11. @Test
  12. public void testProductWithEmptyName() {
  13. }
  14. }

ut测试要自包含

不要使用helper function或者production code里面的变量 比如下面这段代码,将变量隐藏在了helper function里面,会使ut逻辑看不懂
  1. insertIntoDatabase(createProduct());
  2. List actualProducts = requestProductsByCategory();
  3. assertThat(actualProducts).containsOnly(new ProductDTO("1", "Office"));

可以将要使用的变量作为函数的parameter传进函数中,比如下面这段代码是好的

  1. insertIntoDatabase(createProduct("1", "Office"));
  2. List actualProducts = requestProductsByCategory("Office");
  3. assertThat(actualProducts).containsOnly(new ProductDTO("1", "Office"));

也不要使用production code里面的变量,比如下面使用到了RowKeyGenerator.LENGTH这个变量,之后RowKeyGenerator.LENGTH这个变量被改了,代码逻辑也跟着被改了,但是UT仍然可以通过,这样是不符合预期的

  1. @Test
  2. public void testGenerate() {
  3. byte[] rk = RowKeyGenerator.generate(project, metric, dimensions);
  4. String rowKey = new String(rk);
  5. byte[] md5 = RowKeyGenerator.md5(project, metric);
  6. byte[] rowKeyPrefix = new byte[RowKeyGenerator.LENGTH];
  7. System.arraycopy(md5, 0, rowKeyPrefix, 0, RowKeyGenerator.LENGTH);
  8. Assert.assertEquals(rowKey, new String(rowKeyPrefix) + project + "$," + metric + "$,userId:12345$,instanceId:i-abc");
  9. }

而是应该使用下面这段代码

  1. @Test
  2. public void testGenerate() {
  3. byte[] rk = RowKeyGenerator.generate(project, metric, dimensions);
  4. String rowKey = new String(rk);
  5. byte[] md5 = RowKeyGenerator.md5(project, metric);
  6. byte[] rowKeyPrefix = new byte[4];
  7. System.arraycopy(md5, 0, rowKeyPrefix, 0, 4);
  8. Assert.assertEquals(rowKey, new String(rowKeyPrefix) + project + "$," + metric + "$,userId:12345$,instanceId:i-abc");
  9. }

这样,如果RowKeyGenerator.LENGTH改成了5,这个UT就不能通过了,可以避免出现逻辑上的bug

不要使用外部数据库的数据

以下代码使用了外部数据库的数据,当外部数据库的数据没了UT就不能通过了

  1. @Test
  2. public void testGetRange() {
  3. SyncClient client = new SyncClient(endpoint, accessId, accessKey,
  4. instanceName);
  5. Assert.assertEquals(client.getRange(), xxx);
  6. }

而是应该使用Mock的client来处理

  1. @Test
  2. public void testGetRange() {
  3. SyncClient client = Mock(SyncClient.class);
  4. Assert.assertEquals(client.getRange(), xxx);
  5. }

可以使用Mockito这个framework来进行mock的测试:https://site.mockito.org/

不要使用随机变量,System.currentSystemMillis(), Instance.now()这样的变量值

使用这样的变量会使ut变得有时通过有时通不过,这样对维护ut变得非常困难

尽量避免使用Assert.assertTrue或者Assert.assertFalse

比如下面这段代码是不好维护的

  1. @Test
  2. public void testGetRange() {
  3. SyncClient client = Mock(SyncClient.class);
  4. Assert.assertEquals(client.getRange(), xxx);
  5. }

这样,当ut不过的时候,输出只会是

expected: but was:

从错误信息完全看不出来为什么错了

所以应该尽量避免这样的代码,而是应该使用

Assert.equals

  1. @Test
  2. public void testGetRange() {
  3. SyncClient client = Mock(SyncClient.class);
  4. Assert.assertEquals(client.getRange(), xxx);
  5. }