Spock測試框架入門
前言
spock是一款全能型的單元測試框架。說到單元測試,就不得不提mock。mock可以確保單元測試更快、獨立性和確定性。
mock框架可以幫我們模擬外部系統,預編程(mock介面),模擬各種重試條件(生產環境出現
問題,只要拿到對應參數就可以通過mock來模擬重現)等目前有很多mock框架,最出名的應該就是Mockito框架了。對比Mockito框架,Spock提供更完整的解決方案(Mockito還需要介面Junit、PowerMock等),並且支持java、groovy等
介紹
Spock和Java生態
如果你用過了Junit及其相關框架,你會好奇為什麼還要用Spock?
答案是Spock提供了覆蓋Java企業應用的完整的測試生命週期。上面提到的那些歷史框架,是基於某種
需要產生的,比如:Junit單純用於單測,單不提供mock;Mockito提供mock功能,單不提供靜態類的mock;PowerMock提供靜態類mock;
它們之間需要整合,且不容易跟上新的測試場景。Spock提供了全家桶測試,內置了mock和stub功能等,很方便上手。此外Spock是新型
測試框架,有時間和精力去吸取以前框架的優點,避開缺點。
Spock提供以下主要功能
1. 提供Groovy的closures,類似Java8的lambda表達式
2. Mockito對參數匹配有限制。如果在方法中使用匹配器,那麼所有的參數都需要匹配器。Spock不受此限制,可以將實參與實參匹配器混合和匹配
3. Spock區分了Stub和Mock,讓單測更易讀
4. Spock提供了更多詳細的錯誤信息
Mock和Stub
Stub模擬類並可以預編程返回值;Mock模擬類並且可以檢查測試是否完畢哪些方法執行了哪些沒有
Mock的常見對象
1. 資料庫連接
2. Web Service
3. 比較慢的類
4. 返回不確定的類
應用
依賴(pom.xml)
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>1.1-groovy-2.4</version>
<scope>test</scope>
</dependency>
<dependency> <!-- enables mocking of classes (in addition to interfaces) -->
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.6.5</version>
<scope>test</scope>
</dependency>
<dependency> <!-- enables mocking of classes without default constructor (together with
CGLIB) -->
<groupId>org.objenesis</groupId>
<artifactId>objenesis</artifactId>
<version>2.5.1</version>
<scope>test</scope>
</dependency>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<version>1.5</version>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
<configuration>
<useFile>false</useFile>
<includes>
<include>**/*Spec.java</include>
<include>**/*Test.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
基礎應用
實例(Stub)
針對有返回值的用Stub
業務代碼
MyService.java
public interface MyService {
Boolean makeDecision(String request);
}
MyServiceImpl.java
public class MyServiceImpl implements MyService{
public DependencyService dependencyService;
public MyServiceImpl(DependencyService dependencyService){
this.dependencyService = dependencyService;
}
@Override
public Boolean makeDecision(String request) {
return this.dependencyService.make(request);
}
}
DependencyService.java
public interface DependencyService {
Boolean make(String param);
}
DependencyServiceImpl.java
public class DependencyServiceImpl implements DependencyService {
@Override
public Boolean make(String param) {
return "ok".equals(param);
}
}
Spock單測代碼
按照「約定大於配置」,將Spock的單測放於src/test/groovy文件夾下,且以「*Spec」命名
MyServiceImplSpec.groovy
class MyServiceImplSpec extends Specification{
def "testMakeDecision"(){
given: "mock相關準備(參數為A代表決策通過,B代表決策否決)"
DependencyService dependencyService = Stub(DependencyService.class)
String trueParam = "A"
String falseParam = "B"
dependencyService.make(trueParam) >> true
dependencyService.make(falseParam) >> false
and: "可以分多段"
MyService myService = new MyServiceImpl(dependencyService)
when: "執行方法調用"
Boolean trueParamResult = myService.makeDecision(trueParam)
Boolean falseParamResult = myService.makeDecision(falseParam)
then: "結果斷言"
trueParamResult == true
falseParamResult == false
}
}
如果你沒有用過groovy,不用擔心。從上面的代碼可以看到它和Java很像。
此外上面的代碼結構很清晰,及時不懂代碼的也可以看懂上面的意思。通過given、and、when、then, 可以看到它遵循了BDD(Behavior Driven Development)模式,
且這些關鍵字後面可以用易懂的語言描述。
多個方法測試可以將公共變數抽取
private DependencyService dependencyService
private MyService myService
def "setup"(){
dependencyService = Stub(DependencyService.class)
myService = new MyServiceImpl(dependencyService)
}
setup類似於Junit的,在每個測試方面前都執行一次
實例(Mock)
針對沒有返回值的依賴方法使用Mock
業務代碼
DefaultEmailService.java
public class DefaultEmailService {
private CustomerService customerService;
private EmailSender emailSender;
public DefaultEmailService(CustomerService customerService, EmailSender emailSender){
this.customerService = customerService;
this.emailSender = emailSender;
}
public void notifyIfNessary(Customer c){
if (customerService.hasSubscribeEmail(c)){
emailSender.sendEmail(c);
}
}
}
CustomerService.java
public interface CustomerService {
Boolean hasSubscribeEmail(Customer c);
}
EmailSender.java
public interface EmailSender {
void sendEmail(Customer c);
}
EventRecorder.java
public interface EventRecorder {
void recordEvent(Event e);
}
class Event{
public Event(Type type, Long timestamp, String customerName) {
this.type = type;
this.timestamp = timestamp;
this.customerName = customerName;
}
public Type getType() {
return type;
}
public void setType(Type type) {
this.type = type;
}
public Long getTimestamp() {
return timestamp;
}
public void setTimestamp(Long timestamp) {
this.timestamp = timestamp;
}
public String getCustomerName() {
return customerName;
}
public void setCustomerName(String customerName) {
this.customerName = customerName;
}
enum Type{SENT, NOT_SENT}
private Type type;
private Long timestamp;
private String customerName;
}
Spock單測代碼
DefaultEmailServiceSpec.groovy
class DefaultEmailServiceSpec extends Specification{
private DefaultEmailService defaultEmailService
private CustomerService customerService
private EmailSender emailSender
private EventRecorder eventRecorder
private Customer customer
def "setup"(){
customerService = Stub(CustomerService.class)
emailSender = Mock(EmailSender.class)
eventRecorder = Mock(EventRecorder.class)
customer = new Customer("luck")
defaultEmailService = new DefaultEmailService(customerService, emailSender, eventRecorder)
}
def "customer describe email and send email"(){
given: "a customer has describe email, and we will send it"
customerService.hasSubscribeEmail(customer) >> true
when: "we check email should be sent"
defaultEmailService.notifyIfNecessary(customer)
then: "customer has received email"
1 * emailSender.sendEmail(customer)
and: "event has be recorded"
// argument check
1 * eventRecorder.recordEvent({
event -> event.getType() == Event.Type.SENT &&
event.getTimestamp() != null &&
event.getCustomerName() == "luck"
})
}
def "customer dont describe email and send no email"(){
given: "a customer hast describe email, and we will not send it"
customerService.hasSubscribeEmail(customer) >> false
when: "we check email should be not sent"
defaultEmailService.notifyIfNecessary(customer)
then: "customer hast received email"
0 * emailSender.sendEmail(customer)
and: "event has be recorded"
1 * eventRecorder.recordEvent({
event -> event.getType() == Event.Type.NOT_SENT &&
event.getTimestamp() != null &&
event.getCustomerName() == "luck"
})
}
}
通過groovy的closures來判斷參數正確性
高級應用
動態參數
業務代碼
CustomerDao.java
public class CustomerDao {
private BaseDao<Customer> baseDao;
private Logger logger;
public CustomerDao(BaseDao<Customer> baseDao, Logger logger) {
this.baseDao = baseDao;
this.logger = logger;
}
public void saveCustomer(String customerName){
if (customerName == null){
logger.error("Missing customer name");
throw new IllegalArgumentException();
}
Customer c = new Customer(customerName);
baseDao.persist(c);
http://logger.info("Save Customer with id %s", c.getId());
}
}
Spock單測代碼
CustomerDaoSpec.groovy
class CustomerDaoSpec extends Specification{
private CustomerDao customerDao
private BaseDao<Customer> baseDao
private Logger logger
def "setup"(){
baseDao = Stub(BaseDao.class)
logger = Mock(Logger.class)
customerDao = new CustomerDao(baseDao, logger)
}
def "when customer save, customer id will logged"(){
given:
baseDao.persist(_ as Customer) >> { Customer c -> c.setId(123456L) }
when:
customerDao.saveCustomer("Nick")
then:
1 * http://logger.info("Save Customer with id %s", 123456L)
}
}
單元測試報告
<dependency>
<groupId>com.athaydes</groupId>
<artifactId>spock-reports</artifactId>
<version>1.6.1</version>
<scope>test</scope>
<!-- this avoids affecting your version of Groovy/Spock -->
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- // if you dont already have slf4j-api and an implementation of it in the classpath, add this! -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.13</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.13</version>
<scope>test</scope>
</dependency>
通過mvn clean test即可跑單測並生成報告。報告的樣式可以自定義,默認路徑為build/spock-reports
推薦閱讀: