單元測試中的Mock方法,一般是爲了繞開那些依賴外部資源或無關功能的方法調用,使得測試重點可以集中在須要驗證和保障的代碼邏輯上。在定義Mock方法時,開發者真正關心的只有一件事:"這個調用,在測試的時候要換成那個假的Mock方法"。html
然而當下主流的Mock框架在實現Mock功能時,須要開發者操心的事情實在太多:Mock框架如何初始化、與所用的單元測試框架是否兼容、要被Mock的方法是否是私有的、是否是靜態的、被Mock對象是new出來的仍是注入的、怎樣把被測對象送回被測類裏...這些非關鍵的額外工做極大分散了使用Mock工具應有的樂趣。java
週末,在翻github上alibaba的開源項目時,無心間看到了下面這個特立獨行的輕量Mock工具。當前知道這個工具的人應該不多,star人數28(包括本人在內),另外我留意了一下該項目在github上第一次提交代碼時間是2020年5月9日。node
項目地址:https://github.com/alibaba/testable-mock
文檔:https://alibaba.github.io/testable-mock/git
換種思路寫Mock,讓單元測試更簡單。無需初始化,不挑測試框架,甭管要換的方法是被測類的私有方法、靜態方法仍是其餘任何類的成員方法,也甭管要換的對象是怎麼建立的。寫好Mock方法,加個@TestableMock註解,一切通通搞定。github
這是 README
上的描述。掃了一眼項目描述與目錄結構後,就抵制不住誘惑,快速上手玩了一下。因而,就有了這篇划水博客,讓看到的朋友也心癢一下(●´ω`●)。固然,最重要的是若是確實好用的話,能夠在實際項目中用起來,這樣就再也不反感須要Mock的單元測試了。web
完整代碼見本人github:https://github.com/itwild/less/tree/master/less-alibaba/less-testableapi
這裏有一個 WeatherApi
的接口,經過調用第三方接口查詢天氣狀況,以下:bash
import com.github.itwild.less.base.http.feign.WeatherExample; import feign.Param; import feign.RequestLine; public interface WeatherApi { @RequestLine("GET /api/weather/city/{city_code}") WeatherExample.Response query(@Param("city_code") String cityCode); }
CityWeather
查詢具體城市的天氣,以下:框架
import cn.hutool.core.map.MapUtil; import com.github.itwild.less.base.http.feign.WeatherExample; import feign.Feign; import feign.jackson.JacksonDecoder; import feign.jackson.JacksonEncoder; import java.util.HashMap; import java.util.Map; public class CityWeather { private static final String API_URL = "http://t.weather.itboy.net"; private static final String BEI_JING = "101010100"; private static final String SHANG_HAI = "101020100"; private static final String HE_FEI = "101220101"; public static final Map<String, String> CITY_CODE = MapUtil.builder(new HashMap<String, String>()) .put(BEI_JING, "北京市") .put(SHANG_HAI, "上海市") .put(HE_FEI, "合肥市") .build(); private static WeatherApi weatherApi = Feign.builder() .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) .target(WeatherApi.class, API_URL); public String queryShangHaiWeather() { WeatherExample.Response response = weatherApi.query(SHANG_HAI); return response.getCityInfo().getCity() + ": " + response.getData().getYesterday().getNotice(); } private String queryHeFeiWeather() { WeatherExample.Response response = weatherApi.query(HE_FEI); return response.getCityInfo().getCity() + ": " + response.getData().getYesterday().getNotice(); } public static String queryBeiJingWeather() { WeatherExample.Response response = weatherApi.query(BEI_JING); return response.getCityInfo().getCity() + ": " + response.getData().getYesterday().getNotice(); } public static void main(String[] args) { CityWeather cityWeather = new CityWeather(); String shanghai = cityWeather.queryShangHaiWeather(); String hefei = cityWeather.queryHeFeiWeather(); String beijing = CityWeather.queryBeiJingWeather(); System.out.println(shanghai); System.out.println(hefei); System.out.println(beijing); }
運行 main
方法,輸出以下:less
上海市: 不要被陰雲遮擋住好心情 合肥市: 不要被陰雲遮擋住好心情 北京市: 陰晴之間,謹防紫外線侵擾
相信大多數人編寫單元測試時,遇到這種依賴第三方資源時,可能就有點反感寫單元測試了。
下面看看有了 testable-mock
工具,如何編寫單元測試?
CityWeatherTest
文件以下:
import com.alibaba.testable.core.accessor.PrivateAccessor; import com.alibaba.testable.core.annotation.TestableMock; import com.alibaba.testable.processor.annotation.EnablePrivateAccess; import com.github.itwild.less.base.http.feign.WeatherExample; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @EnablePrivateAccess public class CityWeatherTest { @TestableMock(targetMethod = "query") public WeatherExample.Response query(WeatherApi self, String cityCode) { WeatherExample.Response response = new WeatherExample.Response(); // mock天氣接口調用返回的結果 response.setCityInfo(new WeatherExample.CityInfo().setCity( CityWeather.CITY_CODE.getOrDefault(cityCode, cityCode))); response.setData(new WeatherExample.Data().setYesterday( new WeatherExample.Forecast().setNotice("this is from mock"))); return response; } CityWeather cityWeather = new CityWeather(); /** * 測試 public方法調用 */ @Test public void test_public() { String shanghai = cityWeather.queryShangHaiWeather(); System.out.println(shanghai); assertEquals("上海市: this is from mock", shanghai); } /** * 測試 private方法調用 */ @Test public void test_private() { String hefei = (String) PrivateAccessor.invoke(cityWeather, "queryHeFeiWeather"); System.out.println(hefei); assertEquals("合肥市: this is from mock", hefei); } /** * 測試 靜態方法調用 */ @Test public void test_static() { String beijing = CityWeather.queryBeiJingWeather(); System.out.println(beijing); assertEquals("北京市: this is from mock", beijing); } }
運行單元測試,輸出以下:
合肥市: this is from mock 上海市: this is from mock 北京市: this is from mock
從運行結果不難發現,依賴第三方接口的 query
方法已經被僅僅加了個 TestableMock
註解的方法Mock了。也就是說達到了預期的Mock效果,並且代碼優雅易讀。
那麼,這優雅易讀的背後到底隱藏着什麼祕密呢?
相信對這方面有些瞭解的朋友或多或少也猜到了,沒錯,正是字節碼加強技術!!!
package com.alibaba.testable.agent; import com.alibaba.testable.agent.transformer.TestableClassTransformer; import java.lang.instrument.Instrumentation; /** * Agent entry, dynamically modify the byte code of classes under testing * @author flin */ public class PreMain { public static void premain(String agentArgs, Instrumentation inst) { parseArgs(agentArgs); inst.addTransformer(new TestableClassTransformer()); } }
package com.alibaba.testable.agent.handler; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.ClassNode; import java.io.IOException; /** * @author flin */ abstract public class BaseClassHandler implements Opcodes { public byte[] getBytes(byte[] classFileBuffer) throws IOException { ClassReader cr = new ClassReader(classFileBuffer); ClassNode cn = new ClassNode(); cr.accept(cn, 0); transform(cn); ClassWriter cw = new ClassWriter( 0); cn.accept(cw); return cw.toByteArray(); } /** * Transform class byte code * @param cn original class node */ abstract protected void transform(ClassNode cn); }
追一下源碼,可見,該Mock工具藉助了ASM Core API來修改字節碼。上面也提到了,該項目在github上開源出來的時間並不長,核心代碼並很少,認真看應該能看懂,主要是有些朋友可能歷來沒有了解過字節碼加強技術。這裏推薦美團技術團隊的一篇字節碼加強技術相關的文章,https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html,相信有了這樣的基礎,回過頭來再看看 TestableMock
的源碼會輕鬆許多。
本篇博客並不會過多探究字節碼加強技術的細節,頂多算是拋磚引玉,目的是讓讀者知道有這麼一個優雅的Mock工具,另外字節碼加強技術至關因而一把打開運行時JVM的鑰匙,利用它能夠動態地對運行中的程序作修改,也能夠跟蹤JVM運行中程序的狀態,這樣就能在開發中減小冗餘代碼,提升開發效率。順便提一句,咱們平時使用的AOP(Cglib就是基於ASM的)也與字節碼加強密切相關,它們實質上仍是利用各類手段生成符合規範的字節碼文件。
雖然這篇不講修改字節碼的操做細節,但我仍是想讓讀者直觀地看到加強後的字節碼(class文件)是什麼樣子的,說白了就是到底把我寫的代碼在運行時修改爲了啥???因而,我把運行時加強過的字節碼從新寫入了文件,而後使用反編譯工具(拖到IDEA中便可)觀察被修改後的源碼。
運行時(即加強後的)CityWeatherTest.class反編譯後以下:
import com.alibaba.testable.core.accessor.PrivateAccessor; import com.alibaba.testable.core.annotation.TestableMock; import com.alibaba.testable.core.util.InvokeRecordUtil; import com.alibaba.testable.processor.annotation.EnablePrivateAccess; import com.github.itwild.less.base.http.feign.WeatherExample.CityInfo; import com.github.itwild.less.base.http.feign.WeatherExample.Data; import com.github.itwild.less.base.http.feign.WeatherExample.Forecast; import com.github.itwild.less.base.http.feign.WeatherExample.Response; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @EnablePrivateAccess public class CityWeatherTest { CityWeather cityWeather = new CityWeather(); public static CityWeatherTest _testableInternalRef; public static CityWeatherTest _testableInternalRef; public CityWeatherTest() { } @TestableMock( targetMethod = "query" ) public Response query(WeatherApi var1, String cityCode) { InvokeRecordUtil.recordMockInvoke(new Object[]{var1, cityCode}, false); InvokeRecordUtil.recordMockInvoke(new Object[]{var1, cityCode}, false); Response response = new Response(); response.setCityInfo((new CityInfo()).setCity((String)CityWeather.CITY_CODE.getOrDefault(cityCode, cityCode))); response.setData((new Data()).setYesterday((new Forecast()).setNotice("this is from mock"))); return response; } @Test public void test_public() { _testableInternalRef = this; _testableInternalRef = this; String shanghai = this.cityWeather.queryShangHaiWeather(); System.out.println(shanghai); Assertions.assertEquals("上海市: this is from mock", shanghai); } @Test public void test_private() { _testableInternalRef = this; _testableInternalRef = this; String hefei = (String)PrivateAccessor.invoke(this.cityWeather, "queryHeFeiWeather", new Object[0]); System.out.println(hefei); Assertions.assertEquals("合肥市: this is from mock", hefei); } @Test public void test_static() { _testableInternalRef = this; _testableInternalRef = this; String beijing = CityWeather.queryBeiJingWeather(); System.out.println(beijing); Assertions.assertEquals("北京市: this is from mock", beijing); } }
運行時(即加強後的)CityWeather.class反編譯後以下:
import cn.hutool.core.map.MapUtil; import com.github.itwild.less.base.http.feign.WeatherExample.Response; import feign.Feign; import feign.jackson.JacksonDecoder; import feign.jackson.JacksonEncoder; import java.util.HashMap; import java.util.Map; public class CityWeather { private static final String API_URL = "http://t.weather.itboy.net"; private static final String BEI_JING = "101010100"; private static final String SHANG_HAI = "101020100"; private static final String HE_FEI = "101220101"; public static final Map<String, String> CITY_CODE = MapUtil.builder(new HashMap()).put("101010100", "北京市").put("101020100", "上海市").put("101220101", "合肥市").build(); private static WeatherApi weatherApi = (WeatherApi)Feign.builder().encoder(new JacksonEncoder()).decoder(new JacksonDecoder()).target(WeatherApi.class, "http://t.weather.itboy.net"); public CityWeather() { } public String queryShangHaiWeather() { Response response = CityWeatherTest._testableInternalRef.query(weatherApi, "101020100"); return response.getCityInfo().getCity() + ": " + response.getData().getYesterday().getNotice(); } private String queryHeFeiWeather() { Response response = CityWeatherTest._testableInternalRef.query(weatherApi, "101220101"); return response.getCityInfo().getCity() + ": " + response.getData().getYesterday().getNotice(); } public static String queryBeiJingWeather() { Response response = CityWeatherTest._testableInternalRef.query(weatherApi, "101010100"); return response.getCityInfo().getCity() + ": " + response.getData().getYesterday().getNotice(); } public static void main(String[] args) { CityWeather cityWeather = new CityWeather(); String shanghai = cityWeather.queryShangHaiWeather(); String hefei = cityWeather.queryHeFeiWeather(); String beijing = queryBeiJingWeather(); System.out.println(shanghai); System.out.println(hefei); System.out.println(beijing); } }
原來,運行時把調用到 query
方法的實現都換成了本身Mock的代碼。