Groovy
Groovy 是一种带有可选类型系统的动态语言. 借助Groovy语言, 可以在需要强类型时得到类型系统的静态检查保障, 而在需要灵活性时, 享受到Duck Typing 的便利性.
在编写测试代码方面上, Groovy 的优势主要体现在optional syntax rule 和power assertion statement 两个方面上.
-
optional syntax rule. 在Java 中强制的部分语法规则, 如分号, 变量类型, 访问修饰符,在Groovy 中都是可选的.
- 对测试的影响: 跳过Java private 修饰符的封装性, 测试类可以读取被测试类的内部状态.
-
power assertion statement. 提供了强大的多样化的assert.
主要优势: 比Java 更有可读性, 且能够清晰地展示验证失败时的结果.
-
例如, 在Java 中的断言语句:
Assert.isTrue(foo.bar.equals("hello"));
在Goovy 中可以写成这样:
assert foo.bar == "hello"
.更进一步, 使用Spock 测试框架, 可以进一步简写为:
expect: foo.bar == "hello"
Spock
Spock 集成了Junit, JMock 和RSpec 等测试框架的优势, 使开发者能够使用BDD DSL 语言进行测试代码的编写.
它完全兼容Junit, 同时不需要依赖任何的Mock 框架(如Mockito).
关于Spock 技术的更多信息, 请参考Spock Primer.
在这里, 给出Spock 与JUnit 的术语对比表. 以增加大家的直观理解.
Spock | JUnit |
---|---|
Specification | Test class |
setup() |
@Before |
cleanup() |
@After |
setupSpec() |
@BeforeClass |
cleanupSpec() |
@AfterClass |
Feature | Test |
Feature method | Test method |
Data-driven feature | Theory |
Condition | Assertion |
Exception condition | @Test(expected=…) |
Interaction | Mock expectation (e.g. in Mockito) |
实践
在Intellij IDEA 作为IDE, 并使用Gradle 作为工程构建工具.
环境准备
在Intellij 中安装gmavnen intelliJ plugin和spock plugin 两个插件.
-
在
build.gradle
中应用Groovy 插件:apply plugin: 'groovy'
该插件会在编译期间, 编译src/main/groovy 和src/test/groovy 目录下的Groovy 源文件.
-
在
build.gradle
中添加Spock 的依赖:testCompile( ... "org.spockframework:spock-core:$spockCoreVersion", )
对Java 单元测试的改造
-
首先, 这是遗留的使用Java 语言编写的单元测试代码:
@RunWith(SpringJUnit4ClassRunner.class) public class DefaultGatewayInterruptServiceTest { @Mock private GatewayInterruptMapper gatewayInterruptMapper; @Mock private CompanyManageService companyManageService; @Mock private AreaManageService areaManageService; @Mock private RolePermissionManageService rolePermissionManageService; @InjectMocks DefaultGatewayInterruptService service; @Test public void should_return_gateway_interruptions() { List<GatewayInterrupt> interrupts = Lists.newArrayList(); GatewayInterrupt interrupt1 = new GatewayInterrupt(); interrupt1.setCompanyId(1L); interrupt1.setDistrictId(11L); interrupt1.setSiteId(111L); interrupt1.setGatewayId(1111L); interrupt1.setInterruptTime(new GregorianCalendar(2000, 1, 1).getTime()); interrupt1.setRecoveryTime(new GregorianCalendar(2000, 1, 2).getTime()); interrupt1.setStatus(false); interrupts.add(interrupt1); GatewayInterrupt interrupt2 = new GatewayInterrupt(); interrupt2.setCompanyId(1L); interrupt2.setDistrictId(11L); interrupt2.setSiteId(111L); interrupt2.setGatewayId(2222L); interrupt2.setInterruptTime(new GregorianCalendar(2000, 1, 1).getTime()); interrupt2.setStatus(true); interrupts.add(interrupt2); when(gatewayInterruptMapper.getAll()).thenReturn(interrupts); HashMap<Long, Company> companyHashMap = Maps.newHashMap(); Company company = new Company(); company.setName("compnay1"); companyHashMap.put(1L, company); when(companyManageService.getCachedReadOnlyCompanyMap()).thenReturn(companyHashMap); HashMap<Long, District> districtHashMap = Maps.newHashMap(); District district = new District(); district.setName("district1"); districtHashMap.put(11L, district); when(areaManageService.getCachedReadOnlyDistrictMap()).thenReturn(districtHashMap); HashMap<Long, Site> siteHashMap = Maps.newHashMap(); Site site = new Site(); site.setName("site1"); siteHashMap.put(111L, site); when(areaManageService.getCachedReadOnlySiteMap()).thenReturn(siteHashMap); HashMap<Long, Gateway> gatewayHashMap = Maps.newHashMap(); Gateway gateway1 = new Gateway(); gateway1.setId(1111L); gateway1.setName("gateway1"); Gateway gateway2 = new Gateway(); gateway2.setId(2222L); gateway2.setName("gateway2"); gatewayHashMap.put(1111L, gateway1); gatewayHashMap.put(2222L, gateway2); when(areaManageService.getCachedReadOnlyGatewayMap()).thenReturn(gatewayHashMap); List<User> users = Lists.newArrayList(); User user = new User(); user.setName("user1"); users.add(user); when(rolePermissionManageService.getManagerOfSite(any())).thenReturn(users); List<GatewayInterruptDTO> interruptions = service.getGatewayInterruptions(); assertThat(interruptions.size(), is(2)); } }
-
使用Groovy 进行改造后的代码如下:
class DefaultGatewayInterruptServiceSpec extends Specification { def gatewayInterruptMapper = Mock(GatewayInterruptMapper) def companyManageService = Mock(CompanyManageService) def areaManageService = Mock(AreaManageService) def rolePermissionManageService = Mock(RolePermissionManageService) def service = new DefaultGatewayInterruptService (gatewayInterruptMapper, companyManageService, areaManageService, rolePermissionManageService) def "should return gateway interruptions"() { given: def interrupt1 = new GatewayInterrupt( companyId: 1L, districtId: 11L, siteId: 111L, gatewayId: 1111L, interruptTime: new GregorianCalendar(2000, 1, 1).getTime(), recoveryTime: new GregorianCalendar(2000, 1, 2).getTime(), status: false ) def interrupt2 = new GatewayInterrupt( companyId: 1L, districtId: 11L, siteId: 111L, gatewayId: 2222L, interruptTime: new GregorianCalendar(2000, 1, 1).getTime(), status: true ) when: def interruptions = service.getGatewayInterruptions() then: gatewayInterruptMapper.getAll() >> [interrupt1, interrupt2] companyManageService.getCachedReadOnlyCompanyMap() >> [1L: new Company(name: "company1")] areaManageService.getCachedReadOnlyDistrictMap() >> [11L: new District(name: "district1")] areaManageService.getCachedReadOnlySiteMap() >> [111L: new Site(name: "site1")] areaManageService.getCachedReadOnlyGatewayMap() >> [1111L: new Gateway(id: 1111L, name: "gateway1"), 2222L: new Gateway(id: 2222L, name: "gateway2")] rolePermissionManageService.getManagerOfSite(_ as Site) >> [new User(name: "user1")] interruptions.size() == 2 } }
这段代码中体现了Groovy 的强大便利:
-
构造器中能够给字段赋值.
def interrupt1 = new GatewayInterrupt( companyId: 1L, districtId: 11L, siteId: 111L, gatewayId: 1111L, interruptTime: new GregorianCalendar(2000, 1, 1).getTime(), recoveryTime: new GregorianCalendar(2000, 1, 2).getTime(), status: false )
-
List 字面量和Map 字面量:
[interrupt1, interrupt2] [11L: new District(name: "district1")]
-
简洁的Mock 写法:
then: gatewayInterruptMapper.getAll() >> [interrupt1, interrupt2]
-