`

Unitils 学习笔记

    博客分类:
  • Test
阅读更多
源码部分

EasyMock相关的
有三个注解:
  • Mock : 用注解来创建一个宽松的Mock对象.主要用来在一个测试类中, 对一个声明的属性创建一个mock后的对象, 比如在一个测试类中有一个list属性(未赋值), 那么加@Mock对象之后, 那么该list将会被赋值一个mock的list对象. 就是不为null了.
  • AfterCreateMock: 用来对方法进行注解, 使用了该注解的方法, 将在创建一个mock对象之后被调用, 使用场景: 比如实例化一个对象之后可能还需要执行一些方法来做其他的配置什么的.
  • RegularMock: 用注解来创建一个EasyMock对象.主要是将某个测试类的属性实例化为一个mock对象.


几个工具类:
  • LenientMocksControl: 对一些非参数的方法mock进行了简化
  • DummyObjectUtil: 用来创建一个虚拟的对象, 也就是根据制定的类型创建一个对象, 内部使用了cglib(因此创建的是cglib根据字节码新建的类实例), 可以学习cglib用法


一般要测试某某方法, 可以这样写: testMethodNameWhenXXXXX(), 比如这样写:
引用
testNotCreatedWhenNotNull
testCreatedWhenNull

InjectModule 结合@TestedObject来使用, 一般用来对使用了@TestedObject注解的属性进行实例化, 可以避免属性为null的情况
注意:
该方法在@After之后, 测试方法之前被调用, 为了在@Before方法之前做初始化, 我写了一个@CreateMock的方法

@InjectIntoByType 这个注解用来将使用了该注解的属性的值注入到另一个指定对象的与被注解的属性类型相同的属性上, 感觉很绕
该注解还可以和@TestedObject结合起来用, 也就是说将指定的值注入到使用了@TestObject注解的属性对象的某个属性上.

@InjectInto 跟前一个(@InjectIntoByType)类似
比如这样写:
@InjectIntoStatic(target = InjectOnStatic.class, property = "testObjectField.toInject")

给静态类的属性的属性赋值

unitils有个很有用的注解工具类: AnnotationUtils, 比如得到某个类下面所有使用了指定注解的Field, Method集合

ReflectionAssert.assertReflectionEquals这个是untils一直推荐的一个不错的方法, 通过反射来对对象的属性挨个进行比较检查是否相等.

ReflectionAssert 能够支持宽松(lenient)断言, unitils的宽松断言主要在两个方面:
顺序:在测试的时候忽略容器类型中元素的顺序, 这个参数是ReflectionComparatorMode.LENIENT_ORDER
默认值:是否忽略默认值, ReflectionComparatorMode.IGNORE_DEFAULTS, 也就是说, 对于比较的属性, 只有设置了值才参与比较, 否则忽略(默认值不比较).unitils举的例子很好理解, 比如用户对象有firest, last name, 同时还有street信息, 但是我只想对first, street中的某些字段进行比较.则可以这样写:
User actualUser   = new User("John", "Doe", new Address("First street", "12", "Brussels"));
User expectedUser = new User("John",  null, new Address("First street", null,       null));
assertReflectionEquals(expectedUser, actualUser, IGNORE_DEFAULTS);

但是这种比较必须有一个前提, 就是必须将expect放在前, actual放在后.
对时间的宽松断言处理: 这个实际上是对默认值的一个细化. 也就是说如果时间的expect值为null, 那么actual的时间值将不参与比较, 这个只有在没有这事忽略默认值的时候才能体现其价值. 因此也只能针对特殊情况了.

assertLenientEquals()这个方法实际上是指定要使用宽松比较.

assertPropertyLenientEquals and assertPropertyReflectionEquals用来对指定的属性进行比较, 支持属性连写, 比如这样:
assertPropertyLenientEquals("address.street", "First street", user);

cookbook部分

unitils通过模块化的方式来组织各个功能模块, 现在的最新版本是3.1, 里面采用了spring的模块划分方式, 比如unitils-core, unitils-database, unitils-mock等.这样比以前整合在一个工程里面显得更加清晰.
unitils的核心架构中包含moudule, TestListener两个概念, 类似spring中粘连其他开源软件中的FactoryBean概念.可以看成第三方测试工具的一个粘合剂. 其关系是:UnitilsJUnit4TestClassRunner可以持有多个TestListener, 通过TestListener可以在测试运行的不同阶段注入某些功能. 同时某一个TestListener又被一个对应的Module所持有.
Unitils也可以看成一个插件体系结构, TestListener在整个Unitils中又充当了插件中扩展点的角色, 从TestListener这个接口中我们可以看到, 它可以在crateTestObject, before(after)Class, before(after)TestMethod, beforeSetup, afterTeardown的不同地点添加不同的动作.

Unitils还有一种不错的做法, 就是通过配置文件来设定各种配置. 而且配置文件有不同的级别, 全局默认的, 多人协同使用的, 私人自定义等, 这样提供了不错的定制功能.

其过程是:Unitils通过读取配置文件, 加载配置的Module, 然后从每一个Module中获取对应的TestListener, 最后TsetClassRunner持有这些TestListener, 其中有一个Compsite的UnitilsTestListener, 用来封装所有的TestListener.

不过没明白为什么要采用Unitils->Module->TestListener这种设计, 感觉Module是一种多余.

在数据库方面, Unitils提供了两种Module:DatabaseModule and DbUnitModule

DbUnit是一种不错的针对数据库的测试工具, 这里Unitils对其进行了增强, 主要是大量使用标签来指定测试数据, 以及数据的操作策略. 这种方式大家都很熟悉,
这里有几个标签需要说明一下:
@DataSet 用来指定测试数据文件
@TestDataSource 实例化数据源
@Transactional(TransactionMode.ROLLBACK) 指定事务模式, 这里指定在执行完成之后, 回滚

还有几个module我很少用到, 省略一千字
DBManainter
Hibernate
JPA

Spring Module
对于spring容器的加载这里使用了@SpringApplicationContext注解, 该注解用来将spring配置文件注入到指定的ApplicationContext容器中. 它可以注入到属性中, 也可以注入到class中, 当两个一起注入的时候, 可以将公共的配置在基类中注入到class中, 然后在子类中注入仅本类需要的属性.比如这样写:
@SpringApplicationContext("spring-beans.xml")
public class BaseServiceTest extends UnitilsJUnit4 {
}

public class UserServiceTest extends BaseServiceTest {

    @SpringApplicationContext("extra-spring-beans.xml")
    private ApplicationContext applicationContext;     
}

由于spring container的加载都是很耗时的, 因此spring module尽量做到spring container的重用.
另外, 还可以通过编程的方式将spring container纳入spring module中管理.
@SpringApplicationContext
public ConfigurableApplicationContext createApplicationContext(List<String> locations) {
    return new ClassPathXmlApplicationContext(Arrays.asList(locations), false);
}

@SpringApplicationContext
public ConfigurableApplicationContext createApplicationContext() {
    return new ClassPathXmlApplicationContext(Arrays.asList("spring-beans.xml","extra-spring-beans.xml"), false);
}

为了将某个要测试的类与spring中的某个bean关联起来, spring moudle也提供了相应的注解, 这个很好理解, 看例子就能明白:
@SpringBean("userService")
private UserService userService;

@SpringBeanByName
private UserService userService;

@SpringBeanByType
private UserService userService;


Mock Moudule
Unitils为了将各种mock整合到里面, 做了不少工作, 比如mock的创建和verfiy等
能够自动在执行test之前创建mock对象
在mock的使用语法上进行了简化
为了在多个测试方法中对mock进行重用, Unitils会在使用之前对mock复制一份, 从而避免多个测试方法之间对mock对象的污染.
提供了一种叫scenario的东东, 用来对内部的mock使用情况打印日志, 出错信息, 做出反馈.

Unitils提供了Mock接口以及其实现类MockObject, MockObject只需要定义, Unitils会自动初始化.怎么使用看看unitils自带的测试代码, 这里它在@Before中实例化了一下, 用法是这样:
    private MockObject<TestClass> mockObject;
    @Before
    public void setUp() {
        mockObject = new MockObject<TestClass>("testMock1", TestClass.class, this);
    }


Mock接口有几个方法:
T getMock(); 用来生成代理的<T>对象. 该T对象可以注入到测试方法之中, 然后对执行过程和结果进行验证
另一个是是returns()方法, 据个例子比较好明白:
    @MatchStatement
    T returns(Object returnValue);

这种定义也非常巧妙, 在接口方法上加注解.
举个例子就明白了:
mock.returns("aValue").method1();

表示当method1方法被调用的时候将返回aValue值. 这里每次执行都会返回同一个值, 如果希望只执行一次, 可以改用onceReturn();
注意, 如果这样写:
mock.returns("aValue").method1(null);

表示任何参数都可以传递(如果需要参数的话), 这样可以进一步提高灵活性.
这种写法正好与EasyMock顺序想法, 结果放前面, 执行操作方后面. 囧!
    @MatchStatement
    T raises(Throwable exception);

举个例子就明白了:
mock.raises(new MyException()).method1();

当执行方法method1将抛出MyException异常.
    @MatchStatement
    T assertInvoked();

主要用来判断后面接的方法被调用了,  比如这样:
mockMessageService.assertInvoked().sendMessage(alert1);

如果希望你mock的对象不会被任何方法调用, 可以这样写(一般写在@After注解的方法中比较合适):
MockUnitils.assertNoMoreInvocations();

默认对调用情况的检查是没有顺序的(一切以灵活为中心), 如果关注顺序的话, 可以这样写, 表示先发alert1, 再发alert2:
mockMessageService.assertInvokedInSequence().sendMessage(alert1);
mockMessageService.assertInvokedInSequence().sendMessage(alert2);


一个MockBehavior类:
private static class TestMockBehavior implements MockBehavior {

        public int invocationCount = 0;

        public Object execute(ProxyInvocation proxyInvocation) throws Throwable {
            invocationCount++;
            return null;
        }
    }

可以用来统计执行测试. 然后对invocationCount 进行检查是否正确.

对于mock对象方法的参数处理, Unitils采用的策略是:如果参数为null, 表示将忽略用户参数匹配. Unitils基本上都是采用的这种宽松的匹配策略

同时Unitils还提供了一些其他的参数匹配策略类:ArgumentMatchers
看看例子就明白了:
mockSchedulerService.returns(alerts).getScheduledAlerts(notNull(User.class))); // Matches with any not-null object of type User
mockSchedulerService.returns(alerts).getScheduledAlerts(isNull(User.class))); // The argument must be null
mockMessageService.assertInvoked().sendMessage(same(alert1)); // The argument must refer to alert1 instance


值传递, 引用传递
如果直接传值或者使用lenEq / refEq 参数匹配器的话, 将对该参数copy一份然后进行比较, 这样可以避免经过比较之后对参数所作的改变, 而如果使用eq或者same参数匹配器方法的话, 那么则不会copy

自定义参数匹配器
匹配器是一个很少用到的玩意儿, 如果需要自己定义, 可以看看下面的这个例子:
public class CustomArgumentMatchers {

    @ArgumentMatcher
    public static int lessThan(Integer lessThan) {
        ArgumentMatcherRepository.getMock().registerArgumentMatcher(new LessThanArgumentMatcher(lessThan));
        return 0;
    }
}       

public class LessThanArgumentMatcher implements ArgumentMatcher {

    private Integer lessThan;

    public LessThanArgumentMatcher(Integer lessThan) {
        this.lessThan = lessThan;
    }

    public boolean matches(Object argument, Object argumentAtInvocationTime) {
        Integer argumentAsInt = (Integer) argument;
        return lessThan.compareTo(argumentAsInt) < 0;
    }
}

Unitils的所有matcher实现都在org.unitils.mock.argumentmatcher.impl这个包下面, 不知道怎么实现, 也可以参考这里.ArgumentMatchers则用来注册这些Matcher

Dummy 对象
这个跟Mock对象很像, 但是还是有一些区别, 使用@Dummy注解的属性对象, 类似于new了一个对象, 在测试中可以调用该对象的方法. 这些方法的返回值都是默认的(0, false或null). 使用场景:对于这种对象中的内容在测试中不用关心, 但是在测试过程中不允许为空, 而要构造这样的对象又需要一些参数, 而且必须非空的, 这样使用@Dummy注解或者通过MockUnitils.createDummy()方法来创建会简化我们不少工作. 这些对象一般是JavaBean的Domain对象, 因此使用的时候要注意.

查找失败原因利器
在Unitils的org.unitils.mock.report包下面, 有一些用来帮助分析测试失败原因的类, 应该是使用动态代理技术将测试的执行过程打印出来
scenario:用来报告mock对象在执行过程中在哪里被调用
SuggestedAssertsReport:用来打印出所有的检查调用.
Detailed scenario: 这个更细了, 将测试执行过程中所有的参数, 以及返回值都打印出来了.
通过unitils.properties中可以设置这些打印处理:
  • mockModule.logObservedScenario
  • mockModule.logDetailedObservedScenario
  • mockModule.logSuggestedAsserts
  • mockModule.logFullScenarioReport


PartialMock
感觉跟MockObject很像, 看了源代码也没有看出什么不同, 都是通过cglib来生成类, 接口的mock对象. 强悍的cglib.
但是又不是全部都是mock的, 还是允许调用真实的方法, 看了MockObjectPartialMockTest这个测试类可能对PartialMock会有所了解, 也就是如果你对mock对象的方法使用了mock, 比如这样写:
mockObject.returns("aValue").testMethod();

那么mock对象会认为你想使用的是模拟的方法, 而不是真是的方法, 但是如果你这样写:
String result = mockObject.getMock().testMethod();

mock对象会认为你想要调用测试对象真实方法.

Mock注入
在需要将一个mock对象注入到一个真实的测试对象的时候比较合适, 比如下面的做法:
@InjectInto(property="userDao")
private Mock<UserDao> mockUserDao;

@TestedObject
private UserService userService;

这里将userDAO注入到userService中了.
注意这个inject动作是在setup(@Before)之后做的
@TestedObject这个是注入的目标对象, 如果有多个, 都会注入.
下面是根据制定的对象名字注入:
@InjectInto(target="userService", property="userDao")
private Mock<UserDao> mockUserDao;
private UserService userService;

根据类型注入:
@InjectIntoByType
private Mock<UserDao> mockUserDao;

@TestedObject
private UserService userService;


想静态类中注入对象(@InjectIntoStatic and @InjectIntoStaticByType)
跟上面类似, 通常用在将一个mock对象注入到一个单例类的静态属性上.
@InjectIntoStatic(target=UserService.class property="singleton")
private Mock<UserDao> mockUserDao;

对应静态属性这种注入方式, Unitils会在测试完成之后将注入的属性恢复到注入前状态, 保证不影响其他测试用例.
当然对于要恢复到什么状态也可以指定(默认情况下是恢复到赋值前的值):
@InjectIntoStaticByType(target=UserService.class restore=Restore.NULL_OR_0_VALUE)
private Mock<UserDao> mockUserDao;

可以通过属性文件对全局配置进行修改:
引用
InjectModule.InjectIntoStatic.Restore.default=old_value


将mock注入与ServiceLocator(Lookup模式)结合使用
@AfterCreateMock
void injectMock(Object mock, String name, Class type) {
    ServiceLocator.injectService(type, mock);
}

主要是使用了@AfterCreateMock注解, 这个注解跟他的名字一样, 当mock对象实例化的时候, 会调用使用该注解的方法.

EasyMock module
这里主要是几个注解:
@Mock 在@After之前, 将注解的field进行实例化.同时我们可以借助前面的mock注入, 将mock对象注入到其他对象中.
例子如下:
public class UserServiceTest extends UnitilsJUnit4 { 

    @Mock
    private UserDao mockUserDao;

    private UserService userService;
    
    @Before
    public void setUp() {
        userService = new UserService();
        userService.setUserDao(mockUserDao);        
    }

    @Test 
    testDisableInActiveAccounts() {    
        expect(mockUserDao.getAccountsNotAccessedAfter(null)).andReturn(accounts);
        mockUserDao.disableAccount(accounts.get(0));
        mockUserDao.disableAccount(accounts.get(1));
        EasyMockUnitils.replay();

        userService.disableInactiveAccounts(); 
    }
}

默认情况下, EasyMock对象只会检查mock对象是否执行, 而对执行顺序不做检查, 对这些检查条件, 我们可以在注解中进行设置:
@Mock(returns=Calls.LENIENT, invocationOrder=InvocationOrder.STRICT)
private UserDao mockUserDao;

全局属性文件设置:
引用

EasyMockModule.Mock.Calls.default=lenient
EasyMockModule.Mock.InvocationOrder.default=strict


Unitils对EasyMock做了一些增强, 提供反射宽松式的EasyMock:LenientMocksControl, 也就是使用放射方式对参数进行检查比对(跟前面的lenient概念相同), 比如下面的做法被认为是match的:
引用
expected: dao.findById(0);
actual:   dao.findById(99999);

List<Integer> userIds = new ArrayList<Integer>();
userIds.add(3);
userIds.add(2);
userIds.add(1);
expected: dao.deleteById(Arrays.asList(1,2,3));
actual:   dao.deleteById(userIds);

expected: dao.update(0,    new User(null,   "Doe"));
actual:   dao.update(9999, new User("John", "Doe"));


@Mock采用的是Unitils式Mock, 但是如果要使用EasyMock的Mock需要这样写:
@RegularMock
private UserDao mockUserDao;


Uniitls还有很懂东东值得深入, 但是我觉得对mock, dbunit, spring这几个module用熟了应该非常强了.
3
0
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics