前言

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

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 * 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


推薦閱讀:
相關文章