工做多年后我更了解了UT的重要性

对于有经验的开发写单元测试是很是有必要的,而且对本身的代码质量以及编码能力也是有提升的。单元测试能够帮助减小bug泄露,经过运行单元测试能够直接测试各个功能的正确性,bug能够提早发现并解决,因为能够跟断点,因此可以比较快的定位问题,比泄露到生产环境再定位要代价小不少。同时充足的UT是保证重构正确性的有效手段,有了足够的UT防御,才能放开手脚大胆重构已有代码,工 做多年后更了解了UT,了解了UT的重要性。java

单元测试

在敏捷的开发理念中,覆盖全面的自动化测试是添加新特性和重构的必要前提。单元测试在软件开发过程当中的重要性不言而喻,特别是在测试驱动开发的开发模式愈来愈流行的前提下,单元测试更成为了软件开发过程当中不可或缺的部分。同时单元测试也是提升软件质量,花费成本比较低的重要方法。数据库

1.单元测试的时机和测试点

1.1单元测试的时机

  1. 在业务代码前编写单元测试采用测试驱动开发,这是咱们常用和推荐的。
  2. 在业务代码过程当中进行单元测试,对重要的业务逻辑和复杂的业务逻辑进行添加测试。
  3. 在业务逻辑以后再编写测试是咱们不建议的,除非对遗留代码的修改,须要先进行测试用例的添加,保证咱们修改和重构后的代码不会破坏以前的业务逻辑。

1.2单元测试的测试点

  1. 在逻辑复杂的代码中添加测试。
  2. 在容易出错的地方添加测试。
  3. 不易理解的代码中添加测试,在之后看到测试就能够很是清楚代码要实现的逻辑。
  4. 在考虑后期需求变动相对较大的代码中添加测试,这样后期需求更变修改代码以后就不用太担忧写的代码对不对以及是否破坏了已有代码逻辑。
  5. 外部接口处添加解耦代码、同时增长单元测试。

2.代码不可测试性的根源

  1. 代码中调用到了底层平台的接口或只有系统运行后才能得到的资源(数据库链接、发送邮件,网络通信,远程服务, 文件系统等)但业务代码与这些资源未解耦。这样在测试代码须要建立这个类的时候会去初始化这些资源时致使没法测试。
  2. 在方法内部new一个与本次测试无关的对象。
  3. 代码依赖层次很深,逻辑复杂,一次方法的每每要调用N次底层的接口,或者类的方法很是多。这样的代码咱们须要对类进行重构,尽可能保证类的单一职责:这个类在系统中的意图应当是单一的,且修改它的缘由应该只有一个。
  4. 使用单例类和静态方法,而且单例类和静态方法使用到了咱们底层的接口或者其余接口。

3.测试工具使用和测试方法介绍

在作单元测试的时候,咱们会发现咱们要测试的方法会引用不少外部依赖的对象,如调用平台接口、链接数据库、网络通信、远程服务、FTP、文件系统等等。 而咱们无法控制这些外部依赖的对象,为了解决这个问题,咱们就须要用到Mock工具来模拟这些外部依赖的对象,来完成单元测试。
如今比较流行的Mock工具备JMock、EasyMock、Mockito、PowerMock。咱们使用的是Mockito和PowerMock。PowerMock弥补了其余3个Mock工具不能mock静态、final 、私有方法的缺点。
在下面的状况下咱们可使用Mock对象来完成单元测试。缓存

  1. 实对象具备不可肯定的行为,会产生不可预测的结果。 如:数据库查询能够查出一条记录、多条记录、或者返回数据库异常等结果。
  2. 真实对象很难被建立。如:平台代码,或者Web、JBoss容器等。
  3. 真实对象的某些行为很难触发。 如:代码中须要处理的网络异常、数据库异常、消息发送异常等。
  4. 真实状况令程序运行很慢。 在敏捷的实践中咱们完成了CI,在开发提交代码前须要执行整个项目的单元测试用例,只有测试经过才能够提交代码。这就要求咱们每一个单元测试用例须要尽量的短,整个项目的测试时间才会短。当有的测试用例须要测试大数据量状况下系统的预期时,就须要使用Mock对象。
    如咱们代码中须要判断只有当系统的缓存队列大于40000时,咱们开始考虑丢弃非关键的消息,当超过48000时,须要只处理最重要的消息,当超过50000时须要丢弃所有消息。此时就须要对此缓存队列进行Mock,根据调用返回不一样的数据量给测试。
  5. 测试须要知道真实对象是如何被调用的。如:测试用例须要验证是否发送了JMS,此时就能够经过Mock对象是否被调用来测试。
  6. 真实对象实际不存在时。 如:当咱们与其余模块交互时,或者与新的接口打交道时,更有就是对方的代码尚未开发完毕时,咱们能够经过Mock来模拟接口的行为,实现代码逻辑的验证和测试。

3.1 Mocktio简单使用说明

mock能够模拟各类各样的对象,从而代替真正的对象作出但愿的响应。服务器

一、模拟对象的建立网络

List cache = mock(ArrayList.class);
System.out.println(cache.get(0));
//-> null 因为没有对mock对象给预期,因此返回都是null

二、模拟对象方法调用的返回值多线程

List cache = mock(ArrayList.class);
when(cache.get(0)).thenReturn("hello");
System.out.println(cache.get(0));
//-> hello

三、模拟对象方法屡次调用和屡次返回值并发

List cache = mock(ArrayList.class);
when(cache.get(0)).thenReturn("0").thenReturn("1").thenReturn("2");
System.out.println(cache.get(0));
System.out.println(cache.get(0));
System.out.println(cache.get(0));
System.out.println(cache.get(0));
//-> 0,1,2,2 若是实际调用的次数超过了预期的次数,则会一直返回最后一次的预期值。

四、模拟对象方法调用抛出异常app

List cache = mock(ArrayList.class);
when(cache.get(0)).thenReturn(new Exception("Exception"));
System.out.println(cache.get(0));

五、模拟对象方法在没有返回值时也能够抛异常框架

List cache = mock(ArrayList.class);
doThrow(new Exception("Exception")).when(cache).clear();

六、模拟方法调用时的参数匹配ide

AnyInt的使用,匹配任何int参数
List cache = mock(ArrayList.class);
when(cache.get(anyInt())).thenReturn("0");
System.out.println(cache.get(0));
System.out.println(cache.get(2));
//-> 0,0

七、模拟方法是否被调用和调用的次数,预期调用了一次

List cache = mock(ArrayList.class);
cache.add("steven");
verify(cache).add("steven");

预期调用了两次入缓存,没有调用清除缓存的方法

List cache = mock(ArrayList.class);
cache.add("steven");
cache.add("steven");
verify(cache,times(2)).add("steven");
verify(cache,never()).clear();

还能够经过atLeast(int i)和atMost(int i)来替代times(int i)来验证被调用的次数最小值和最大值。
【注意】
Mock对象默认状况下,对于全部有返回值且没有预期过的方法,Mocktio会返回相应的默认值。对于内置类型会返回默认值,如int会返回0,布尔值返回false。对于其余type会返回null。mock对象会覆盖整个被mock的对象,所以没有预期的方法只能返回默认值。这个在初次使用Mock时须要注意,常常会发现测试结果不对,最后才发现本身未给相应的预期。

3.2 PowerMock简单使用说明

PowerMock使用一个自定义类加载器和字节码操做来模拟静态方法,构造函数,final类和方法,私有方法,去除静态初始化器等等。
PowerMock使用简单,在类名前添加注解,在预期前调用PowerMock的mock静态类方法,其余的预期方法和Mockito相似。

@PrepareForTest(System.class)
@RunWith(PowerMockRunner.class)
public class Test {
@org.junit.Test
public void should_get_filed() {
    System.out.println(System.getProperty("myName"));
    PowerMockito.mockStatic(System.class);
    PowerMockito.when(System.getProperty("myName")).thenReturn("steven");
    System.out.println(System.getProperty("myName"));
    //->null steven
    }
}

3.3 Fake对象的使用

测试中须要模拟对象,除了经常使用的mock对象外,咱们还会常常用到Fake对象。Mock对象是预先计划好的对象,带有各类期待,他们组成了一个关于他们期待接受的调用的详细说明。而Fake对象是有实际可工做的实现,可是一般有一些缺点致使不适合用于产品,咱们一般使用Fake对象在测试中来模拟真实的对象。
在测试中常常会发现咱们须要使用系统或者平台给咱们提供的接口,在测试中咱们能够新建立一个类去实现此接口,而后在根据具体状况去实习此模拟类的相应方法。

如咱们建立了本身的FakeLog对象来模拟真实的日志打印,这样咱们能够在测试类中使用FakeLog来代替代码中真实使用的Log类,能够经过FakeLog的方法和预期的结果比较来进行测试正确性的判断。

Fake对象和mock对象还有一个实际中使用的区别,Fake对象咱们构造好后,之后全部的代码都去调用此Fake对象就能够了,不用每一个类每次都要给预期。从这个角度能够看到当一个类的方法或者预期相对不变时,能够采用Fake对象,当这个类的返回信息预期变化很是不可预期时,能够采用MOCK对象。

3.4Mock服务的两种方式

(1)直接注入:用于类之间的依赖层次较多的状况,测试整个业务流程,粒度大。

ResourceServerService service = mock(ResourcePPUServerService.class);
new Processor().process(service );

(2)重写protected方法返回mock对象:用于类直接依赖于该服务的状况,测试行为的细节,粒度小。

ResourceServerService service = mock(ResourceServerService .class);
generator = new EutranAnrDeletingItemGenerator() {
    @Override
    protected ResourceServerService getService() {
        return service;
    }
}

3.5测试异常

Throwable有两个直接子类:Exception和Error

一、expcetd=SomeExecption.class

@Test(expected = AssertionError.class)
public void should_occur_assertion_error_when_emb_number_is_not_eutran_or_utran_anr_delete() throws Exception {
    EMBObject eMBObject = new EMBObject();
    new AnrDeleteProcessor().getAnrDeleteGenerator(EMBObject);
}

@Test(expected = NumberFormatException.class)
public void should_throw_number_format_exception_when_input_string_field_greater_255() {
    TransactionIDConvert.convertTransIDToLong(transactionError);
}

二、try-catch-fail只能用于Exception,Error不能用此种方式

try {
    method.invoke();
    fail();
} catch (Exception e) {
    assertTrue(e.getCause() instanceof RuntimeException);
}

3.6私有方法—采用反射来调用

@Test
public void should_throw_runtime_exception_when_check_eutran_trap_data_fail() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    when(eutranAnrAddItemGenerator.getSrvCelProcessor()).thenReturn(processor);
    when(processor.validateTrapData(any(AnrItem.class), any(AnrBean.class))).thenReturn(false);

    Method method = EutranAnrAddItemGenerator.class.getDeclaredMethod("check", AnrItem.class);
    method.setAccessible(true);
    try {
        method.invoke(eutranAnrAddItemGenerator, anrAddItem);
    } catch (Exception e) {
        assertTrue(e.getCause() instanceof RuntimeException);
    }
}

4.单元测试的格式

4.1测试类结构

public class ExampleTest {
    @BeforeClass
    public static void setUp() throws Exception {
        initGlobalParameter();
        registerServices();
    }

    @Before
    public void setUp() throws Exception {
        initGlobalParameter();
        registerServices();
    }

    @After
    public void tearDown() throws Exception {
        ServiceAccess.serviceMap.clear();
        clearCache();     }

    @AfterClass
    public static void tearDown() throws Exception {
        ServiceAccess.serviceMap.clear();
        clearCache();
    }

    @Test
    public void should_get_some_result1_when_give_some_condition1{
    }

    @Test
    public void should_get_some_result2_when_give_some_condition2{
    }
}

JUnit4是JUnit框架有史以来的最大改进,其主要目标即是利用Java5的Annotation特性简化测试用例的编写。先简单解释一下什么是Annotation,这个单词通常是翻译成元数据。元数据是什么?元数据就是描述数据的数据。也就是说,这个东西在Java里面能够用来和public、static等关键字同样来修饰类名、方法名、变量名。修饰的做用描述这个数据是作什么用的,差很少和public描述这个数据是公有的同样。

  • @Before:每一个测试方法执行以前都要执行一次。
  • @After:before对应,每一个测试方法执行以后要执行一次。
  • @BeforeClass:在全部测试方法以前运行,只运行一次。通常在此类中申请昂贵的外部资源。父类中有@BeforeClass方法,在其子类运行以前也会运行。
  • @AfterClass:与BeforeClass对应,在全部测试结束后,释放BeforeClass中申请的资源。 注意:@Before,@After,@BeforeClass,@AfterClass 标示的方法一个类中只能各有一个
  • @Test: 告诉JUnit,该方法要做为一个测试用例来运行。

4.2测试代码的位置

在Java中一个包能够横跨两个不一样的目录,因此咱们的测试代码和产品代码放在同一目录中,这样维护起来更方便,测试代码和产品代码在同一个包中,这样也减小了没必要要的包引发,同时在测试类中使用继承更加的方便。

4.3测试用例格式3段式

一个测试用例主体内容通常采用三段式:given-when-then

  • Given:构造测试条件;
  • When:执行待测试的方法;
  • Then:判断测试结果是否符合指望。
    例如:
@Test
public void should_get_correct_result_when_add_two_numbers() {
    int a = 1;
    int b = 2;

    int c = MyMath.add(a, b);

    assertEquals(3, c);
}

4.4类名的命名方式

测试类的名称以Test结尾。从目标类的类名衍生出其单元测试类的类名。类名前加上Test后缀。
Fake(伪类)放在测试包中,使用前缀Fake。

4.5方法名的定义方式

should …do something…when…under some conditions…

例如:

should_NOT_delete_A_when_exists_B_related_with_A
should_throw_exception_when_the_parameter_is_illegal

4.6业务代码中为测试提供的方法的注解

在业务代码中为了测试而单独提供的保护方法或者其余方法,咱们经过@ForTest来标注。FofTest类以下:

@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE})
public @interface ForTest {
    String description() default "";
}

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

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

5.1数据库

数据库的单元测试,因为测试没法进行数据库的链接,故咱们经过提取通用接口(DBManagerInterface)和FakeDBManager来实现数据库解耦。FakeDBManager能够对真实的数据库进行模拟,也就是咱们经过Fake一个简单的内存数据库来模拟实际真实的数据库。
DBManager是咱们的真实链接数据库的业务类。咱们在测试时,是能够经过注入的方式用FakeDBManager来替换DBManager。

5.2平台接口

5.2.1 平台接口的Mock

平台中的MinosMmlPPUServerService、ResourcePPUServerService等服务接口,均可以经过mock来进行测试。须要注意的是在业务代码中须要进行相应的解耦,能够经过SET方法或者构造器来注入平台的服务类。

public class ICMEMBMessageListenerTest {
    private MinosMmlPPUServerService  minosMmlPPUServerService = mock(MinosMmlPPUServerService.class);

@Before
public void setUp() throws Exception {
    registerServices();
    icmembMessageListener = new ICMEMBMessageListener(){
    };
    when(minosMmlPPUServerService.getIp()).thenReturn("127.0.0.1");
    when(minosMmlPPUServerService.getPort()).thenReturn("80");
    when(minosMmlPPUServerService.getEmbPort()).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);
    I18nAnrOsf.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<Integer> 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);
    }
}

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

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

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);
    }
}

二、增长新方法

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

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

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

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

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);
    }
}

六、增长对新方法的测试用例

public class DataProcessorTest {
    private DataProceeor dataProceeor;
    private SomePlateService somePlateService;
    private Map<String, String> attributes;

    @Before
    public void setUp() throws Exception {
        attributes.put("pci", "1");
    }

    @Test
    public void should_get_attributes() {
        somePlateService = mock(SomePlateService.class);
        when(somePlateService.queryAttribue()).thenReturn(attributes);

        dataProceeor = new DataProceeor();

        CompensateData compensateData = dataProceeor.getAttributes("pci");
        assertThat(compensateData.value(), is("1"));
        assertThat(compensateData.value(), is("2"));
    }
}

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

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

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

public class NotificationDispatcher {
    private static Logger logger = LoggerFactory.getLogger(NotificationDispatcher.class);

    public void processMessage(String notificationMsg) {
        NotificationMsg notification = new Gson().fromJson(notificationMsg, NotificationMsg.class);
        Map<String, String> sctpInfo;
        try {
            sctpInfo = new NotificationParser().parse(notification.getMessage());
            logger.info("Parse notification xml success: " + sctpInfo);
            NotificationProcessor processor = new NotificationProcessorFactory().getProcessor(sctpInfo.get(CONFIGURATION_TYPE));
            processor.process(sctpInfo);
        } catch (Exception e) {
            logger.warn(String.format("Deal notification failed. Exception: %s", e.getMessage()), e);
        }
    }
}

在本例中,查询MOI的Filter是在getCellMoi方法内部构造出来的,咱们能够尝试给getCellMoi方法编写测试用例:
测试用例没有经过,问题出在哪里呢?
Debug代码发现,在getCellMoi方法内部构造出来的Filter和咱们在测试代码中构造的Filter并非同一个对象。很天然地想到为Filter类编写子类,并覆盖其equals方法。
用自定义的Filter代替平台的Filter:

public String getCellMoi(String cellName){
    Filter filter = new SelfFilter(cellName);
    return getAttributers(filter,"moi");
}

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

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

在模式2中,因为Filter是在getCellMoi内部构造的,而且没有euqals方法,致使没法测试。还能够用别的方法对其进行改造。代码示例以下:
1.提取protected方法buildFilter()

public void processMessage(String notificationMsg) {
    UmeNotificationMsg umeNotificationMsg = new Gson().fromJson(notificationMsg, UmeNotificationMsg.class);
    Map<String, String> sctpInfo;
    try {
        sctpInfo = new NotificationParser().parse(umeNotificationMsg.getMessage());
        logger.info("Parse notification xml success: " + sctpInfo);
        NotificationProcessor processor = getNotificationProcessorFactory().getProcessor(sctpInfo.get(CONFIGURATION_TYPE));

        processor.process(sctpInfo);
    } catch (Exception e) {
        logger.warn(String.format("Deal notification failed. Exception: %s", e.getMessage()), e);
    }
}

@ForTest
protected NotificationProcessorFactory getNotificationProcessorFactory() {
        return new NotificationProcessorFactory();
}

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

@Before
public void setUp() throws Exception {
    NotificationProcessorFactory notificationProcessorFactory = mock(NotificationProcessorFactory.class);
    notificationDispatcher = new NotificationDispatcher(){
        @Override
        protected NotificationProcessorFactory getNotificationProcessorFactory() {
            return notificationProcessorFactory;
        }
    };
}

运行测试,能够经过。

八、总结

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

相关文章
相关标签/搜索