单元测试二

如何设计单元测试代码?

设计原则

  • 命名规范
    确保测试用例名字中包括被测试的场景和期望的输出。

  • 单一职责
    测试类一次仅测试一个类,测试方法一次仅测试一个方法。

  • 行为而不是方法
    测试用例与被测方法并非是“一一对应”关系,我们真正想要验证的是软件的行为,而一个方法可以表现出很多种行为。

单测工具

单元测试二_第1张图片
image.png

测试桩(Stub):

public class LoggerStub implements Logger{
    @Override
    public void log(LogLevel logLevel, String message) {
    }
    
    public LogLevel getLogLevel() {
        return LogLevel.WARN;
    }
}

伪造对象(Fake):

public interface UserRepository {
    void save(User user);
    User findById(int id);
    User findByUserName(String userName);
}
public class FakeUserRepository implements UserRepository{
    private List users = new ArrayList();

    @Override
    public void save(User user) {
        if (findById(user.getId()) == null) {
            user.add(user);
        }
    }
    @Override
    public User findById(int id) {
        for (User user: users) {
            if (user.getId() == id) return user;
        }
        return null;
    }

    @Override
    public User findByUserName(String userName) {
        for (User user: users) {
            if (user.getName().equals(userName)) return user;
        }
        return null;
    }
}

测试间谍(Spy):

 public String concat(String first, String second) {
        return first + ":" + second;
 }
public void write(Level level, String message) {
        for (DLogTarget dLogTarget : targets) {
            dLogTarget.write(level, message);
        }
    }
public class DLog {
    private final DLogTarget[] targets;

    public DLog(DLogTarget... targets) {
        this.targets = targets;
    }

    public void write(Level level, String message) {
        for (DLogTarget dLogTarget : targets) {
            dLogTarget.write(level, message);
        }
    }
}
public class DLogTest {
    @Test
    public void writesEachMessageToAllTargets() throws Exception {
        // 间谍
        SpyTarget spyTarget1 = new SpyTarget();
        SpyTarget spyTarget2 = new SpyTarget();
        // 加入
        DLog dLog = new DLog(spyTarget1, spyTarget2);
        dLog.write(Level.INFO, "message");
        // 断言结果
        assert(spyTarget1.received(Level.INFO, "message"));
        assert(spyTarget2.received(Level.INFO, "message"));
    }

   // 实现间谍类
    private static class SpyTarget implements DLogTarget {
        private List log = new ArrayList<>();

        private String concatenated(Level level, String message) {
            return level.getName() + ":" + message;
        }

        // 接受信息
        @Override
        public void write(Level level, String message) {
            log.add(concatenated(level, message));
        }

        // 读取信息
        boolean received(Level level, String message) {
            return log.contains(concatenated(level, message));
        }
    }
}

模拟对象:

@RunWith(MockitoJUnitRunner.class)
public class MockUnitTest {
    @Mock
    Context mContext;

    @Test
    public void getAppNameByContext() {
        when(mContext.getString(R.string.app_name)).thenReturn("MockUnitTest");
        assertEquals(mContext.getString(R.string.app_name), "MockUnitTest");
    }
}

单测常见问题

基本断言:

单元测试二_第2张图片
image.png

问题:断言目标不清晰直接,此代码测试目的,是为了验证文本某一行是否包含了期望的字符。
但这里使用角标、assertTrue、!= 使逻辑复杂化,以及魔数-1,使接受的人猜测,为什么要与-1比较,如果不是-1是否会出现其他问题

单元测试二_第3张图片
image.png

修改一:使用函数,尽量不使用!=等方式

单元测试二_第4张图片
image.png

修改二:此例关键问题在于抽象层次的不正确,即使用基本类型来表达更高层次的概念。当引入!=或==、魔数-1或0等的断言,就应该考虑是否是抽象层次不对。

过度断言:

public class LogFileTransformerTest {
    private String expectedOutput;
    private String logFile;
    @Before
    public void setUpBuildLogFile(){
        StringBuilder lines = new StringBuilder();
        lines.append("[2015-05-23 21:20:33] session-di###SID");
        lines.append("[2015-05-23 21:20:33] user-id###UID");
        lines.append("[2015-05-23 21:20:33] screen1");  
        
        logFile = lines.toString();
    }
    @Before
    public void setUpBuildTransformedFile(){
        StringBuilder lines = new StringBuilder();
        lines.append("session-di###SID"); 
        lines.append("user-id###UID"); 
        lines.append("screen1");
      
        expectedOutput = lines.toString();
    }
    @Test
    public void testTransformationGeneratesRgiht(){
        TransfermationGenerator generator = new TransfermationGenerator();
        File outputFile = generator.transformLog(logFile);
        Assert.assertTrue("目标文件转换后不存在!", outputFile.exists());
        Assert.assertEquals("目标文件转换后不匹配!", expectedOutput, getFileContent(outputFile));
    }
}

问题:这是测试TransfermationGenerator类的日志转换功能是否有效。但对于最后一个断言,expectedOutput转后失败,其包含的可能原因有多个,这违背了设计的单一职责原则,测试的意图也不明确

@Test
public void testTransformationGeneratesRgiht2(){
    TransfermationGenerator generator = new TransfermationGenerator();
    File outputFile = generator.transformLog(logFile);
    Assert.assertTrue("目标文件转换后不匹配!", getFileContent(outputFile).contains("screen1###0"));
    Assert.assertTrue("目标文件转换后不匹配!", getFileContent(outputFile).contains("screen1###51"));
}

@Test
public void testTransformationGeneratesRgiht3(){
    TransfermationGenerator generator = new TransfermationGenerator();
    File outputFile = generator.transformLog(logFile);
    String outputString = getFileContent(outputFile);
    Assert.assertTrue(outputString .indexof("screen1") > outputString.indexof(user-id###UID));
}

修改:将断言进行细分,根据不同的情况创建单独的测试方法

突显关键语句:

单元测试二_第5张图片
image.png

问题:数据的初始化和关键测试模块混淆在一起,可读性太差,需要花更多的时间才能找到关键测试代码。

单元测试二_第6张图片
image.png

修改:将初始化数据抽取,放在setUp方法中,使测试的感觉代码更加清晰突显。此示例是在同一个测试行为中表现的结构不清晰,进一步可能导致不同行为混淆在一个方法中测试

不同行为混淆:

示例一:

单元测试二_第7张图片
image.png

问题:不同的行为断言,在同一个方法里进行,违背了单一职责原则。

单元测试二_第8张图片
image.png

修改:新建不同的测试方法,对不同行为进行测试。如果不同的行为足够复杂,可创建新的类进行管理,如示例二。

示例二:

单元测试二_第9张图片
image.png

问题:一是,Configuration可在初始化中构建;二是,文件名、调试、警告、信息开关、版本号显示以及空的命令行参数列表混淆在同一个方法中测试

单元测试二_第10张图片
image.png
单元测试二_第11张图片
image.png

修改:将不同的行为抽取到单独的测试类中,继承是否会导致逻辑缺乏内聚,取决于测试类是否从基类中共享了不变的测试

逻辑缺乏内聚:

单元测试二_第12张图片
image.png

问题:初始化数据的位置,距离测试代码有200多行,不容易找到数据来源,其次,初始化数据和测试方法逻辑分散

单元测试二_第13张图片
image.png

修改:在“靠近”测试方法的位置初始化数据。

何时内联数据或逻辑?
1. 如若短小,则内联
2. 如若过长,则将其藏到工厂方法或测试数据构造器背后
3. 如果不变,则拉进单独文件

魔法数字

单元测试二_第14张图片
image.png

问题:使用魔数无法得知为什么会产生300

单元测试二_第15张图片
image.png

修改:使用常量清晰地命名

单元测试二_第16张图片
image.png

问题:在初始化setUp方法中做太多事情

单元测试二_第17张图片
image.png

过度保护:

单元测试二_第18张图片
image.png

问题:当data为空时,断言处仍旧会抛出NullPointException

image.png

可维护性

  • 重复
单元测试二_第19张图片
image.png

问题:字符串重复,进而导致结构重复

单元测试二_第20张图片
image.png
单元测试二_第21张图片
image.png

修改:抽取结构方法作为替换

  • 条件逻辑
单元测试二_第22张图片
image.png

问题:对于测试代码中条件语句,容易认为某个短语通过了,但实际没有执行到

单元测试二_第23张图片
image.png

修改:for循环替换成自定义断言,添加失败反馈。应尽量避免if、else、while等条件语句

  • 不确定性
单元测试二_第24张图片
image.png

问题:无法得知sleep多久能保证线程执行完成,而且sleep会造成测试缓慢


单元测试二_第25张图片
image.png

修改:去除sleep,每个线程工作结束后,再通知测试线程

你可能感兴趣的:(单元测试二)