模擬對象一般都叫 Mock 或 Stub, 兩者差不多, 都是模擬被測組件對外依賴的模擬, 存根 stub 就在那里, 不需要檢查它和被測組件的交互, Mock 則可以用來檢查于被測對象的交互 MockMock 是測試驅(qū)動開發(fā)必備之利器, 只要有狀態(tài), 有依賴, 做單元測試就不能沒有 Mock Mock 的原則Mockito 是廣泛使用的 Java Mock library, 它的 wiki 上有篇文章 - 如何寫出好的測試代碼, 其中提出了幾條使用 mock 的原則:
后兩點很好理解, 第一點有些語焉不詳, 什么叫非你所有的類型, 我的理解就是如果一個類型不是你與第三方約定的接口, 它屬于別人定義的, 你只是拿過來使用, 那么你最好不要去mock 它, 你可以寫一個中間層或適配器, 然后mock 這個中間層和適配器, 原因是第三方可以隨時更改它的定義和行為, 你把它mock掉了, 你也就不會發(fā)現(xiàn)由于別人更改了定義或行為導(dǎo)致的異常. 而你自己寫的中間層由你掌控,不必有此擔(dān)心。 與第三方或其它服務(wù)集成測試屬于Consumer Test 消費者測試和End to End 端到端的測試的范圍 Mock 使用步驟
驗證什么呢, 除了你的程序的應(yīng)用邏輯, 還有對于所mock的對象的交互驗證
Mock 的問題mock的時候最煩人的是兩個問題 1.無法mock就我熟悉的, 也是應(yīng)用最廣的兩門語言 C 和 Java 來看 gmock 和 mockito 在大多數(shù)情況下都夠用了,一般情況下不需要也不應(yīng)該 mock 私有方法,靜態(tài)方法和全局方法,當然如果你的代碼可測試性及依賴反轉(zhuǎn)做得得沒那么好, 實在繞不過去,也有權(quán)變之法, C 可以直接改掉其在內(nèi)存中的函數(shù)地址, Java 可以利用反射或修改字節(jié)碼來搞定. 2. 需要mock的太多了
我也寫了一個類似于 hub 的類, 所有消息會回調(diào)到一個 MessageReceiver, MessageReceiver 會直接調(diào)用注冊上來的各個 MessageHandler, 每個 Handler 只關(guān)注自己關(guān)心的消息, 具體來說, 每個 Handler 都可以設(shè)置一個正則表達式, 當消息頭或消息體匹配這個正則表達式, 則由這個 Handler 來處理回應(yīng)事先 mock好的消息, 回應(yīng)你自己指定的消息, 從而把這個系統(tǒng)對外的依賴全部 mock 掉, 并測試了所有的交互 mock 的粒度根據(jù)你測試的對象大小,粒度自然有區(qū)別,根據(jù)測試三角形,小而美,越大越麻煩, 從小到大可以分為如下三個粒度 1. mock一個函數(shù)與這個函數(shù)的交互全部mock 掉 2. mock整個類或接口與這個類或接口的交互全部mock 掉,接口也可指某個API 3. mock 整個系統(tǒng)與系統(tǒng)外部的交互全部mock 掉 總之,模擬外部依賴要區(qū)分內(nèi)外的邊界,找到合適的切入點 Mock 類庫和工具僅就我所熟悉的 Java 和 C 舉例如下, python, ruby, JavaScript 之類的腳本語言就更簡單了 Mockito for JavaPowermock for Javahttps://github.com/powermock/powermock 它通過自定義類加載器和修改字節(jié)碼來mock static methods, constructors, final classes and methods, private methods, removal of static initializers 等等 GoogleMock for Chttps://github.com/google/googletest/tree/master/googlemock Mock ServerMockServer 用來 mock 整個web service wiremockWireMock 和上面的 mock server差不多, 是一個 HTTP-based APIs的模擬器. 典型示例接下來, 讓我們寫幾個例子來說明 mock 和相關(guān)類庫的用法... Mock 依賴的類和方法基本步驟:
這里以 Guava Loading Cache 類為例, 測試它的基本行為是否符合預(yù)期 package com.github.walterfan.hellotest;import com.google.common.cache.CacheBuilder;import com.google.common.cache.CacheLoader;import com.google.common.cache.LoadingCache;import com.google.common.cache.RemovalCause;import com.google.common.cache.RemovalListener;import com.google.common.cache.RemovalNotification;import com.google.common.util.concurrent.Uninterruptibles;import lombok.extern.slf4j.Slf4j;import org.mockito.ArgumentCaptor;import org.mockito.Captor;import org.mockito.Mock;import org.mockito.Mockito;import org.mockito.MockitoAnnotations;import org.mockito.invocation.InvocationOnMock;import org.mockito.stubbing.Answer;import org.testng.annotations.BeforeMethod;import org.testng.annotations.Test;import java.util.HashMap;import java.util.Map;import java.util.Optional;import java.util.concurrent.ExecutionException;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicInteger;import static org.mockito.Mockito.times;import static org.mockito.Mockito.verify;import static org.testng.Assert.assertEquals;import static org.testng.Assert.assertTrue;/**
* Created by yafan on 23/1/2018.
*/@Slf4jpublic class LoadingCacheTest { private LoadingCache<String, String> internalCache; @Mock
private CacheLoader<String, String> cacheLoader; @Mock
private RemovalListener<String, String> cacheListener; @Captor
private ArgumentCaptor<RemovalNotification<String, String>> argumentCaptor; private Answer<String> loaderAnswer; private AtomicInteger loadCounter = new AtomicInteger(0); @BeforeMethod
public void setup() {
MockitoAnnotations.initMocks(this); this.internalCache = CacheBuilder.newBuilder()
.maximumSize(3)
.expireAfterWrite(1, TimeUnit.SECONDS)
.removalListener(this.cacheListener)
.build(this.cacheLoader); this.loaderAnswer = new Answer<String>() { @Override
public String answer(InvocationOnMock invocationOnMock) throws Throwable {
String key = invocationOnMock.getArgumentAt(0, String.class); switch(loadCounter.getAndIncrement()) { case 0: return 'alice'; case 1: return 'bob'; case 2: return 'carl'; default: return 'unknown';
}
}
};
} @Test
public void cacheTest() throws Exception { //Mock the return value of loader
//Mockito.when(cacheLoader.load(Mockito.anyString())).thenReturn('alice');
Mockito.when(cacheLoader.load(Mockito.anyString())).thenAnswer(loaderAnswer);
assertTrue('alice'.equals(internalCache.get('name'))); //sleep for 2 seconds
Uninterruptibles.sleepUninterruptibly(2, TimeUnit.SECONDS);
assertTrue('bob'.equals(internalCache.get('name')));
verify(cacheLoader, times(2)).load('name');
verify(cacheListener).onRemoval(argumentCaptor.capture());
assertEquals(argumentCaptor.getValue().getKey(), 'name');
assertEquals(argumentCaptor.getValue().getValue(), 'alice');
assertEquals(argumentCaptor.getValue().getCause(), RemovalCause.EXPIRED);
}
} Mock 靜態(tài)方法這里使用 Powermock 和 testng , 如果有 junit 的話, 用法稍有不同
package com.github.walterfan.hellotest;import org.junit.runner.RunWith;import org.mockito.Mockito;import org.powermock.api.mockito.PowerMockito;import org.powermock.core.classloader.annotations.PrepareForTest;import org.powermock.modules.junit4.PowerMockRunner;import org.powermock.modules.testng.PowerMockObjectFactory;import org.powermock.modules.testng.PowerMockTestCase;import org.testng.IObjectFactory;import org.testng.annotations.Test;import java.io.File;import java.io.FileFilter;import java.util.Arrays;import java.util.List;import static org.mockito.Matchers.eq;import static org.testng.Assert.assertEquals;//@RunWith(PowerMockRunner.class) -- for junit4@PrepareForTest(FileUtils.class)public class FileUtilsTest extends PowerMockTestCase { public int howManyFiles(String path, FileFilter filter) {
System.out.println('-----------');
List<String> files = FileUtils.listFiles(new File(path), filter);
files.forEach(System.out::println); return files.size();
} @Test
public void testHowManyFiles() {
List<String> fileNames = Arrays.asList('a.java', 'b.java', 'c.java');
PowerMockito.mockStatic(FileUtils.class);
PowerMockito.when(FileUtils.listFiles(Mockito.any(), Mockito.any())).thenReturn(fileNames); int count = howManyFiles('.', FileUtils.javaFileFilter);
assertEquals(count, 3);
}
} 在 pom.xml 中加上
Mock 第三方服務(wù)假設(shè)我們在服務(wù)啟動時需要調(diào)用第三方的服務(wù)來獲取訪問口令 GET $third_service_url/oauth2/api/v1/access_token?client_id=$clientId&client_secret=$clientPass 返回值是 json : { 'token': '$token'} 我們在本地做測試時并沒有部署這個第三方服務(wù), 我們可以用如下方法 mock 掉整個第三方服務(wù)的所有 API 調(diào)用, 例子代碼如下, 這里用到了以上所說的 http://www. 庫 ![]() package com.github.walterfan.hellotest;import lombok.extern.slf4j.Slf4j;import okhttp3.Headers;import okhttp3.OkHttpClient;import okhttp3.Request;import okhttp3.Response;import org.apache.http.HttpHeaders;import org.mockserver.integration.ClientAndServer;import org.mockserver.matchers.Times;import org.mockserver.model.HttpRequest;import org.mockserver.model.HttpResponse;import org.testng.annotations.AfterSuite;import org.testng.annotations.BeforeSuite;import org.testng.annotations.Test;import java.io.IOException;import static org.assertj.core.api.Assertions.assertThat;import static org.junit.Assert.assertEquals;import static org.mockserver.model.HttpResponse.response;import static org.testng.Assert.assertTrue;@Slf4jpublic class MockServerTest { public static final String ACCESS_TOKEN_URL = '/oauth2/api/v1/access_token'; public static final String ACCESS_TOKEN_RESP = '{ \'token\': \'abcd1234\'}'; private int listenPort; private OkHttpClient httpClient; //mock server
private ClientAndServer mocker;
public MockServerTest() {
listenPort = 10086;
httpClient = new OkHttpClient();
} //啟動 mock server
@BeforeSuite
public void startup() {
mocker = ClientAndServer.startClientAndServer(listenPort);
} //關(guān)閉 mock server
@AfterSuite
public void shutdown() {
mocker.stop(true);
} @Test
public void testCheckHealth() throws IOException {
HttpRequest mockReq = new HttpRequest().withMethod('GET').withPath(ACCESS_TOKEN_URL);
HttpResponse mockResp = new HttpResponse().withStatusCode(200).withBody(ACCESS_TOKEN_RESP).withHeader(HttpHeaders.CONTENT_TYPE, 'application/json;charset=UTF-8'); //mock API 的返回
mocker.when(mockReq, Times.exactly(1))
.respond(mockResp);
String theUrl = String.format('http://localhost:%d%s?%s' , listenPort, ACCESS_TOKEN_URL, 'client_id=test&client_secret=pass');
Request request = new Request.Builder()
.url(theUrl)
.build();
Response response = httpClient.newCall(request).execute();
assertTrue(response.isSuccessful());
Headers responseHeaders = response.headers(); for (int i = 0; i < responseHeaders.size(); i ) {
log.info(responseHeaders.name(i) ': ' responseHeaders.value(i));
} //mock server 返回了之前設(shè)定的結(jié)果
String strResult = response.body().string();
log.info(' strResult: {}', strResult);
assertEquals(strResult, ACCESS_TOKEN_RESP); //驗證 mock 的交互
mocker.verify(mockReq);
}
} 輸出如下
pom.xml 如下 <?xml version='1.0' encoding='UTF-8'?><project xmlns='http://maven./POM/4.0.0' xmlns:xsi='http://www./2001/XMLSchema-instance'
xsi:schemaLocation='http://maven./POM/4.0.0 http://maven./xsd/maven-4.0.0.xsd'>
<modelVersion>4.0.0</modelVersion>
<groupId>com.github.walterfan</groupId>
<artifactId>hellotest</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>hellotest</name>
<description>Demo project for Mock Test</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.8.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Dalston.SR4</spring-cloud.version>
<okhttp.version>3.8.0</okhttp.version>
<mock-server-version>5.3.0</mock-server-version>
<maven-shade-plugin-version>2.1</maven-shade-plugin-version>
<metrics.version>3.1.5</metrics.version>
</properties>
<dependencies>
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-core</artifactId>
<version>${metrics.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-wiremock</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>6.11</version>
</dependency>
<dependency>
<groupId>org.mock-server</groupId>
<artifactId>mockserver-netty</artifactId>
<version>${mock-server-version}</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp.version}</version>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock</artifactId>
<version>2.12.0</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build></project> 參考資料 |
|
來自: liang1234_ > 《自動化測試》