工作多年后我更明白了UT的重要性(下)

简介: 对于有经验的开发写单元测试是非常有必要的,并且对自己的代码质量以及编码能力也是有提高的。单元测试可以帮助减少bug泄露,通过运行单元测试可以直接测试各个功能的正确性,bug可以提前发现并解决,由于可以跟断点,所以能够比较快的定位问题,比泄露到生产环境再定位要代价小很多。同时充足的UT是保证重构正确性的有效手段,有了足够的UT防护,才能放开手脚大胆重构已有代码,工 作多年后更了解了UT,了解了UT的重要性。 

5.代码中涉及外部接口时,如何来编写单元测试


我们的代码涉及的模块非常众多,经常需要相互协作来完成一个功能,在此过程中经常需要使用到外部的接口、同时也为别的模块提供服务。


5.1数据库


数据库的单元测试,由于测试无法进行数据库的连接,故我们通过提取通用接口(DBManagerInterface)和FakeDBManager来实现数据库解耦。FakeDBManager可以对真实的数据库进行模拟,也就是我们通过Fake一个简单的内存数据库来模拟实际真实的数据库。 DBManager是我们的真实连接数据库的业务类。我们在测试时,是可以通过注入的方式用FakeDBManager来替换DBManager。


5.2平台接口


5.2.1 平台接口的Mock


平台中的服务接口,都可以通过mock来进行测试。需要注意的是在业务代码中需要进行相应的解耦,可以通过SET方法或者构造器来注入平台的服务类。

public class ListenerTest {
    private ServerService  service = mock(ServerService.class);
@Before
public void setUp() throws Exception {
    when(service.getIp()).thenReturn("127.0.0.1");
    when(service.getPort()).thenReturn("80");
    when(service.getTcpPort()).thenReturn("8080");
}

此处需要注意如果用到静态变量全局唯一的,需要在使用后在 tearDown中进行清除。


5.3 文件接口的测试


我们的业务中也会出现与外部文件进行读写的代码。按照单元测试书写的原则,单元测试应该是独立的,不依赖于外部任何文件或者资源的。好的单元测试是运行速度快,能够帮助我们定位问题。所以我们普通涉及到外部文件的代码,都需要通过mock来预期其中的信息,如MOCK(I18n)文件或者properties、xml文件中的数据。 对于一些重要的文件,考虑到资源消耗不大的情况下,我们也会去为这些文件添加单元测试。需要访问真实的文件,我们第一步就需要去获取资源文件的具体位置。通过下面的FileService的getFileWorkDirectory我们可以获取单元测试运行时的根目录。    


public class FileService {
public static String getFileWorkDirectory() {
    return new StringBuilder(getFileCodeRootDirectory()).append("test").toString();
}
public static String getFileCodeRootDirectory() {
    String userDir = System.getProperty("user.dir");
    userDir = userDir.substring(0, userDir.indexOf(File.separator + "CODE" + File.separator));
    StringBuilder workFilePath = new StringBuilder(userDir);
    workFilePath.append(File.separator).append("CODE").append(File.separator);
    return workFilePath.toString();
}
}

我们在单元测试中可以通过传入具体的文件名称,可以在测试代码中访问真实的文件。 这种方法可以适用I18n文件,xml文件, properties文件。 我们在对I18n文件进行测试时,也可以通过Fake对象根据具体的语言来进行国际化信息的测试。具体FakeI18nWrapper的代码在第7章中给出可以参考。

@Before
public void setUp() throws Exception {
    String i18nFilePath = FileService.getFileWorkDirectory() + "\\conf\\i18n.xml";
    I18N i18N = new FakeI18nWrapper(new File(i18nFilePath), I18nLanguageType.en_US);
    I18nOsf.setTestingI18NInstance(i18N);
}


6.单元测试中涉及多线程、单例类、静态类的处理


6.1多线程测试


通过单元测试,能较早地发现 bug 并且能比不进行单元测试更容易地修复bug。但是普通的单元测试方法(即使当彻底地进行了测试时)在查找并行 bug 方面不是很有效。这就是为什么在实验室测试没有问题,但在外场经常出现各种莫名其妙的问题。 为什么单元测试经常遗漏并行 bug?通常的说法是并行程序和Bug的问题在于它们的不确定性。但是对于单元测试目的而言,在于并行程序是非常 确定的。所以我们单元测试需要对关键的逻辑、涉及到并发的场景进行多线程测试。 多线程的不确定性和单元测试的确定的预期确实是有点矛盾,这就需要精心的设计单元测试中的多线程用例。 Junit本身是不支持普通的多线程测试的,这是因为Junit的底层实现上是用System.exit退出用例执行的。


JVM都终止了,在测试线程启动的其他线程自然也无法执行。所以要想编写多线程Junit测试用例,就必须让主线程等待所有子线程执行完成后再退出。我们一般的方法是在主测试线程中增加sleep方法,这种方法优点是简单,但缺点是不同机器的配置不一样,导致等待时间无法确定。更为高效的多线程单元测试可以使用JAVA的CountDownLatch和第三方组件GroboUtils来实现。 下面通过一个简单的例子来说明下多线程的单元测试。 测试的业务代码如下,功能是唯一事务号的生成器。


class UniqueNoGenerator {
    private static int generateCount = 0;
    public static synchronized int getUniqueSerialNo() {
        return generateCount++;
    }
}


6.1.1 Sleep


private static Set results = new HashSet<>();

@Test
public void should_get_unique_no() throws InterruptedException {
    Thread[] threads = new Thread[100];
    for (int i = 0; i < threads.length; i++) {
    threads[i] = generateThread();
    }
    //启动线程
    Arrays.stream(threads).forEach(Thread::start);
    Thread.sleep(100L);
    assertEquals(results.size(), 100);
 }
private Thread generateThread() {
    return new Thread(() -> {
        int uniqueSerialNo = UniqueNoGenerator.getUniqueSerialNo();
        results.add(uniqueSerialNo);
    });
}

通过Sleep来等待测试线程中的所有线程执行完毕后,再进行条件的预期。问题就是用户无法准确的预期业务代码线程执行的时间,不同的环境等待的时间也是不等的。由于需要添加延时,同时也违背了我们单元测试执行时间需要尽量短的原则。


6.1.2 ThreadGroup


private static Set<Integer> results = new HashSet<>();
private ThreadGroup threadGroup = new ThreadGroup("test");
@Test
public void should_get_unique_no() throws InterruptedException {
    Thread[] threads = new Thread[100];
    for (int i = 0; i < threads.length; i++) {
    threads[i] = generateThread();
 }
    //启动线程
    Arrays.stream(threads).forEach(Thread::start);
    while (threadGroup.activeCount() != 0) {
    Thread.sleep(1);
    }
    assertEquals(results.size(), 100);
    }
    private Thread generateThread() {
    return new Thread(threadGroup, () -> {
    int uniqueSerialNo = UniqueNoGenerator.getUniqueSerialNo();
    results.add(uniqueSerialNo);
    });
}

这个是通过ThreadGroup来实现多线程测试的,可以把需要测试的类放入一个线程组,同时去判断线程组中是否还有未结束的线程。测试中需要注意把新建的线程加入到线程组中。


6.1.3 CountDownLatch


private static Set<Integer> results = new HashSet<>();
private CountDownLatch countDownLatch = new CountDownLatch(100);
@Test
public void should_get_unique_no() throws InterruptedException {
    Thread[] threads = new Thread[100];
    for (int i = 0; i < threads.length; i++) {
        threads[i] = generateThread();
    }
    //启动线程
    Arrays.stream(threads).forEach(Thread::start);
    countDownLatch.await();
    assertEquals(results.size(), 100);
}
private Thread generateThread() {
    return new Thread(() -> {
        int uniqueSerialNo = UniqueNoGenerator.getUniqueSerialNo();
        results.add(uniqueSerialNo);
        countDownLatch.countDown();
    });
}

通过JAVA的CountDownLatch可以很方便的来判断,测试中的线程是否已经执行完毕。CountDownLatch是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待,我们这里是让测试主线程等待。countDown方法是当前线程调用此方法,则计数减一。awaint方法,调用此方法会一直阻塞当前线程,直到计时器的值为0。


6.2单例类测试


单例模式要点:


  1. 单例类在一个容器中只有一个实例。
  2. 单例类使用静态方法自己提供向客户端提供实例,自己拥有自己的引用。
  3. 必须向整个容器提供自己的实例。


  单例类的实现方式有多种方式,如懒汉式单例、饿汉式单例、登记式单例等。我们这里采用内部类的形式来构造单例类,实现的优点是此种方式不需要给类或者方法添加锁,唯一实例的生成是由JAVA的内部类生成机制保证。    下面的例子构造了一个单例类,同时这个单例类我们提供了一个获取远程Cpu信息的方法。再构造一个使用类ResourceManager.java来模拟调用此单例类,同时看下我们测试ResourceManager.java过程中遇到的问题。    单例类DBManagerTools.java:

public class DbManager {
         private DbManager() {
         }
         public static DbManager getInstance() {
         return DbManagerHolder.instance;
         }
         private static class DbManagerHolder {
         private static DbManager instance = new DbManager();
         }
         public String getRemoteCpuInfo(){
         FtpClient ftpClient = new FtpClient("127.0.0.1","22");
         return ftpClient.getCpuInfo();
         }
     }

调用类 ResourceManager.java:

public class ResourceManager {
    public String getBaseInfo() {
        StringBuilder buffer = new StringBuilder();
        buffer.append("IP=").append("127.0.0.1").append(";CPU=").append(DbManager.getInstance().getRemoteCpuInfo());
        return buffer.toString();
    }
}
测试类 
@Test
public void should_get_cpu_info() {
    String expected = "IP=127.0.0.1;CPU=Intel";
    ResourceManager resourceManager = new ResourceManager();
    String baseInfo = resourceManager.getBaseInfo();
    assertThat(baseInfo, is(expected));
}

从上面的描述可以看到,由于业务代码强关联了一个单例类,同时这个单例类会去通过网络获取远程机器的信息。这样我们的单元测试在运行中就会去连接网络中的服务器导致测试失败。在业务类中类似这种涉及到单例类的调用经常用到。 这种情况下我们需要修改下业务代码使代码可测。 第一种方法:提取方法并在测试类中复写。

public class ResourceManager {
    public String getBaseInfo() {
        StringBuilder buffer = new StringBuilder();
        buffer.append("IP=").append("127.0.0.1").append("CPU=").append(getRemoteCpuInfo());
        return buffer.toString();
    }
    @ForTest
    protected String getRemoteCpuInfo() {
        return DbManager.getInstance().getRemoteCpuInfo();
    }
}
@Test
public void should_get_cpu_info() {
    String expected = "IP=127.0.0.1;CPU=Intel";
    ResourceManager resourceManager = new ResourceManager(){
        @Override
        protected String getRemoteCpuInfo() {
            return "Intel";
        }
    };
    String baseInfo = resourceManager.getBaseInfo();
    assertThat(baseInfo, is(expected));
}

第二种方法:提取单例类中的方法为接口,然后在业务代码中通过set方法或者构造器注入到业务代码中。

public class DbManager implements ResourceService{
    private DbManager() {
    }
    public static DbManager getInstance() {
        return DbManagerHolder.instance;
    }
    private static class DbManagerHolder {
        private static DbManager instance = new DbManager();
    }
    @Override
    public String getRemoteCpuInfo(){
        FtpClient ftpClient = new FtpClient("127.0.0.1","22");
        return ftpClient.getCpuInfo();
    }
public interface ResourceService {
    String getRemoteCpuInfo();
}
public class ResourceManager {
    private ResourceService resourceService = DbManager.getInstance();
    public String getBaseInfo() {
        StringBuilder buffer = new StringBuilder();
        buffer.append("IP=").append("127.0.0.1").append("CPU=").append(resourceService.getRemoteCpuInfo());
        return buffer.toString();
    }
    public void setResourceService(ResourceService resourceService) {
        this.resourceService = resourceService;
    }
}
@Test
public void should_get_cpu_info() {
    String expected = "IP=127.0.0.1;CPU=Intel";
    ResourceManager resourceManager = new ResourceManager();
    DbManager mockDbManager = mock(DbManager.class);
    resourceManager.setResourceService(mockDbManager);
    when(mockDbManager.getRemoteCpuInfo()).thenReturn("Intel");
    String baseInfo = resourceManager.getBaseInfo();
    assertThat(baseInfo, is(expected));
}

通过上面的方法可以方便的解开业务代码对单例的强依赖,有时候我们发现我们的业务代码是静态类,这个时候你会发下第一种方法是解决不了问题的,只能通过第2中方法来实现。 通过上面的代码可以看到我们应该尽量的少用单例,在必须使用单例时可以设计接口来进行业务与单例类的解耦。


6.3静态类测试


静态类与单例类类似,也可以通过提取方法后通过复现方法来解耦,同样也可以通过服务注入的方式来实现。也可以使用PowerMock来预期方法的返回。 实际应用中如果单例类不需要维护任何状态,仅仅提供全局访问的方法,这种情况考虑可以使用静态类,静态方法比单例更快,因为静态的绑定是在编译期就进行的。 同时需要注意的是不建议在静态类中维护状态信息,特别是在并发环境中,若无适当的同步措施而修改多线程并发时,会导致坏的竞态条件。 单例与静态主要的优点是单例类比静态类更具有面向对象的能力,使用单例,可以通过继承和多态扩展基类,实现接口和更有能力提供不同的实现。 在我们开发过程中考虑到单元测试,还是需要谨慎的使用静态类和单例类。


7.代码可测性的解耦方法


在使用一些解依赖技术时,我们常常会感觉到许多解依赖技术都破坏了原有的封装性。但考虑到代码的可测性和质量,牺牲一些封装性也是可以的,封装本身也并不是最终目的,而是帮助理解代码的。下面在介绍下常用的解依赖方法。这些解依赖方法的思想都是通用的,采用控制反转和依赖注入的方式来进行。


7.1尽量减少业务代码与平台代码之间的耦合


软件开发中调用平台服务查询资源属性的典型代码:

public class DataProceeor{
    private static final SomePlatFormService service = ServerService.lookup(SomePlatFormService.ID);
    public static CompensateData getAttributes(String name){
        service.queryCompensate(name);
    }
}

这种代码在实现上没有问题,但是无法进行单元测试(不启动软件)。因为此类加载时需要获取平台查询资源相关的服务,业务代码与平台代码存在强耦合性。 在不破坏原有功能的基础上对这段代码做如下改造


1、引入实例变量和构造器


public class DataProceeor{
    private static final SomePlatformService service = ServerService.lookup(SomePlatformService.ID);
    private SomePlatformService _service;
    public DataProceeor(SomePlatformService service) {
        _service = service;
    }
    public DataProceeor() {
        _service = ServerService.lookup(SomePlatformService.ID);;
    }
    public CompensateData getAttributes(String name){
        service.queryCompensate(name);
    }
}

2、增加新方法

public CompensateData getSomeAttributes(String name){
    _service.queryCompensate(name);
}

3、查找代码中所有用到方法getAttributes的地方,全部替换成getSomeAttributes。

4、完成第3步后,删除已经无用的变量和方法。

5、重命名引入的变量和方法,使其符合命名规范。

public class DataProceeor{
    private SomePlatformService service;
    public DataProceeor(SomePlatformService service){
        this.service = service;
    }
    public DataProceeor() {
        service = ServerService.lookup(SomePlatformService.ID);;
    }
    public static CompensateData getAttributes(String name){
        service.queryCompensate(name);
    }
}

6、增加对新方法的测试用例

public class DataProcessorTest {
    private DataProceeor dataProceeor;
    private SomePlateService somePlateService;
    private Map<String, String> attributes;
    @Before
    public void setUp() throws Exception {
        attributes.put("value", "1");
    }
    @Test
    public void should_get_attributes() {
        somePlateService = mock(SomePlateService.class);
        when(somePlateService.queryAttribue()).thenReturn(attributes);
        dataProceeor = new DataProceeor();
        Data data = dataProceeor.getAttributes("value");
        assertThat(data.value(), is("1"));
        assertThat(data.value(), is("2"));
    }
}

运行该测试用例,发现最后一句断言没有通过: 修改最后一句断言为:assertThat(data.value(), not("2")); 再次运行测试,测试用例通过。


7.2 扩展平台的部分类,实现测试的目的


模式1中的例子查询资源属性时没有设置过滤条件,事实上大多数处理都是依赖其他处理类:

public class ClassA {
    public void processA () {
            ClassBProcessor processor new ClassBProcessor();
            processor.processB();
        } catch (Exception e) {
            logger.warn(e); 
        }
    }
}

在本例中,processB方法的Filter是在processA方法内部构造出来的,我们可以尝试给processA方法编写测试用例: 测试用例没有通过,问题出在哪里呢? Debug代码发现,在processA方法内部构造出来的Filter和我们在测试代码中构造的Filter并不是同一个对象。很自然地想到为Filter类编写子类,并覆盖其equals方法。 用自定义的ClassBProcessor代替平台的ClassBProcessor:


public String getClassBProcessor(){
    ClassBProcessor filter = new SelfClassBProcessor();
    return filter);
}

修改后测试用例运行通过。


7.3 巧用protedted方法实现测试的注入


在模式2中,由于ClassBProcessor是在processA内部构造的,并且没有euqals方法,导致无法测试。还可以用别的方法对其进行改造。代码示例如下: 1.提取protected方法buildProcessor()    

public class ClassA {
    public void processA () {
            ClassBProcessor processor new ClassBProcessor();
            processor.processB();
        } catch (Exception e) {
            logger.warn(e); 
        }
    }
}
@ForTest
protected ClassBProcessor buildProcessor() {
        return new ClassBProcessor();
}

2.在测试代码中重写buildProcessor方法

@Before
public void setUp() throws Exception {
    private ClassBProcessor classBProcessor;
    ClassA classA = new ClassA(){
        @Override
       protected ClassBProcessor buildProcessor() {
            return classBProcessor;
        }
    };
}

运行测试,可以通过。


8、总结


UT是开发人员的利器,是开发的前置保护伞,也是写出健壮代码的有力保证,总之一句话不会写UT的开发不是好厨子

目录
相关文章
|
6月前
|
芯片
2023年的技术总结和工作反思
一、回顾2023年 回顾自己的2023年,还是发生了很多的变化。在大学毕业,就来到了芯翼参加工作,在这里也遇到了很多的前辈和小伙伴,收获工作的同时也收获了友情。但是,随着公司发展战略的变化,公司的人员架构也变额很多,对于我们刚毕业的大学生也变得越来越不友好,其实我也清楚这就是社会的发展现状。 其实,这不是我最终产生离职想法的结果,最终让我决定离职的是公司新来的人事主管十分的不理解我们,总是处处针对我们,这对于专心搞技术研发的我们来说,无疑是一个定时炸弹,让我们觉得自己的工作没有意义,甚至是没有成绩和结果,总是挂在嘴边的KPI考核也是越来越严格,总是觉得刚毕业的大学生的能力不行之类的,话说谁
|
5月前
|
数据建模
技术经验解读:ZVS振荡电路工作原理分析
技术经验解读:ZVS振荡电路工作原理分析
102 1
|
Arthas 消息中间件 人工智能
为什么很多人工作3年,却只有1年经验?
同样是在软件开发行业工作 3 年,为什么有些人经验丰富,可以独当一面,而有些人却还和工作一年的人差不多?作者给出了自己的答案。
48337 16
|
5月前
|
算法 Java 大数据
为什么很多人工作 3 年 却只有 1 年 经验?
为什么很多人工作 3 年 却只有 1 年 经验?
67 0
|
11月前
|
设计模式 运维 分布式计算
工作经验小结(2023.11.21)
工作经验小结(2023.11.21)
180 1
|
程序员 开发工具
[老文拾遗]如果我当上技术经理如何展开工作(三)
[老文拾遗]如果我当上技术经理如何展开工作(三)
|
缓存 Java 测试技术
工作多年后我更明白了UT的重要性(上)
对于有经验的开发写单元测试是非常有必要的,并且对自己的代码质量以及编码能力也是有提高的。单元测试可以帮助减少bug泄露,通过运行单元测试可以直接测试各个功能的正确性,bug可以提前发现并解决,由于可以跟断点,所以能够比较快的定位问题,比泄露到生产环境再定位要代价小很多。同时充足的UT是保证重构正确性的有效手段,有了足够的UT防护,才能放开手脚大胆重构已有代码,工 作多年后更了解了UT,了解了UT的重要性。 
329 0
|
测试技术 芯片 异构计算
研发感悟:从CPU架构图谈谈开发工作
研发感悟:从CPU架构图谈谈开发工作
173 0
研发感悟:从CPU架构图谈谈开发工作
|
机器学习/深度学习 数据采集 人工智能
2020年,这个算法团队都干了啥?
什么是算法?什么是广告算法工程师?算法工程师又是如何定义的?今天作者将就算法、电商算法为主题和我们分享他的理解,同时还将和我们分享ICBU算法团队的整体工作和2020年的一些重要技术突破。
2020年,这个算法团队都干了啥?

相关实验场景

更多
下一篇
无影云桌面