文章中的所有代碼在此:https://github.com/geniusmart/LoveUT 鲁纠,由于 Robolectric 3.0 和 3.1 版本(包括后續(xù)3.x版本)差異不小总棵,該工程中包含這兩個版本對應(yīng)的測試用例 Demo 。
一 閑話單元測試
我們經(jīng)常講“前人種樹改含,后人乘涼”情龄,然而在軟件開發(fā)中,往往呈現(xiàn)出來的卻是截然相反的景象捍壤,我們在績效和指標(biāo)的驅(qū)使下骤视,主動或被動的留下來大量壞味道的代碼,在短時間內(nèi)順利的完成項(xiàng)目鹃觉,此后卻花了數(shù)倍于開發(fā)的時間來維護(hù)此項(xiàng)目专酗,可謂“前人砍樹,后人遭殃”盗扇,諷刺的是祷肯,砍樹的人往往因?yàn)閮?yōu)秀的績效,此時已經(jīng)步步高升粱玲,而遭殃的往往是意氣風(fēng)發(fā)躬柬,步入職場的年輕人,如此不斷輪回抽减。所以允青,為了打破輪回,從一點(diǎn)一滴做起吧卵沉,“樹”的種類眾多颠锉,作為任意一名普通的軟件工程師,種好單元測試這棵樹史汗,便是撒下一片蔭涼琼掠。
關(guān)于單元測試,很多人心中會有以下幾個疑問:
(1)為什么要寫停撞?
(2)這不是QA人員該做的嗎瓷蛙?
(3)需求天天變悼瓮,功能都來不及完成了,還要同時維護(hù)代碼和UT艰猬,四不四傻昂岜ぁ?
(4)我要怎么寫UT(特別是Android單元測試)冠桃?
- 關(guān)于第一個問題命贴,首先我們反問自己幾個問題:
-
(1)我們在學(xué)習(xí)任何一個技術(shù)框架,比如 retofit2 食听、 Dagger2 時胸蛛,是不是第一時間先打開官方文檔(或者任意文檔),然后查閱api如何調(diào)用的代碼樱报,而官方文檔往往都會在最醒目的地方葬项,用最簡潔的代碼向我們說明了api如何使用?
其實(shí),當(dāng)我們在寫單元測試時迹蛤,為了測試某個功能或某個api玷室,首先得調(diào)用相關(guān)的代碼,因此我們留下來的便是一段如何調(diào)用的代碼笤受。這些代碼的價值在于為以后接手維護(hù)/重構(gòu)/優(yōu)化功能的人,留下一份程序猿最愿意去閱讀的文檔敌蜂。
(2)當(dāng)你寫單元測試的時候箩兽,是不是發(fā)現(xiàn)很多代碼無法測試?撇開對UT測試框架不熟悉的因素之外章喉,是不是因?yàn)槟愕拇a里一個方法做了太多事情汗贫,或者代碼的封裝性不夠好,或者一個方法需要有其他很多依賴才能測試(高耦合)秸脱,而此時落包,為了讓你的代碼可測試,你是不是會主動去優(yōu)化一下代碼摊唇?
(3)是不是對重構(gòu)沒信心咐蝇?這個話題太老生常談了,配備有價值的巷查、高覆蓋率的單元測試可解決此問題有序。
(4)當(dāng)你在寫Android代碼(比如網(wǎng)絡(luò)請求和DB操作)的時候,是如何測試的岛请?跑起來整個App旭寿,點(diǎn)了好幾步操作后,終于到達(dá)要測試的功能崇败,然后巨慢無比的Debug盅称?如果你寫UT,并使用Robolectric這樣的框架,你不僅可以脫離Android環(huán)境對代碼進(jìn)行調(diào)試缩膝,還可以很快速的定位和Debug你想要調(diào)試的代碼混狠,大大的提升了開發(fā)效率。
以上逞盆,便是寫好單元測試的意義檀蹋。
關(guān)于第二個問題,己所不欲勿施于人
我始終覺得讓QA寫UT云芦,是一種傻叉的行為俯逾。單元測試是一種白盒測試,本來就是開發(fā)分內(nèi)之事舅逸,難道讓QA去閱讀你惡心的充滿壞味道的代碼桌肴,然后硬著頭皮寫出UT?試想一下琉历,你的產(chǎn)品經(jīng)理讓你畫原型寫需求文檔坠七,你的領(lǐng)導(dǎo)讓你去市場部輔助吹噓產(chǎn)品,促進(jìn)銷售旗笔,你會不會有種吃了翔味巧克力的感覺彪置?所以,己所不欲勿施于人蝇恶。這個問題有點(diǎn)頭疼拳魁,總之,盡量提高我們的代碼設(shè)計(jì)和寫UT的速度撮弧,以便應(yīng)對各種不合理的需求和項(xiàng)目潘懊。
前面三個問題,或多或少是心態(tài)的問題贿衍,調(diào)整好心態(tài)授舟,認(rèn)可UT的優(yōu)點(diǎn),嘗試走第一步看看贸辈。而第四個問題释树,如何寫?則是筆者這系列文章的核心內(nèi)容裙椭,在我的第一篇《Robolectric3.0(一)》中已經(jīng)介紹了這個框架的特點(diǎn)躏哩,環(huán)境搭建,三大組件(Activity揉燃、Bordercast扫尺、Service)的測試,以及Shadow的使用炊汤,這篇文章正驻,主要介紹網(wǎng)絡(luò)請求和數(shù)據(jù)庫相關(guān)的功能如何測試弊攘。
二 日志輸出
Robolectric對日志輸出的支持其實(shí)非常簡單,為什么把它單獨(dú)列一個條目來講解姑曙?因?yàn)橥覀冊趯慤T的過程襟交,其實(shí)也是在調(diào)試代碼,而日志輸出對于代碼調(diào)試起到極大的作用伤靠。我們只需要在每個TestCase的setUp()里執(zhí)行ShadowLog.stream = System.out即可捣域,如:
@Before
public void setUp() throws URISyntaxException {
//輸出日志
ShadowLog.stream = System.out;
}
此時,無論是功能代碼還是測試代碼中的 Log.i()之類的相關(guān)日志都將輸出在控制面板中宴合,調(diào)試起功能來焕梅,簡直爽得不要不要的。
三 網(wǎng)絡(luò)請求篇
關(guān)于網(wǎng)絡(luò)請求卦洽,筆者采用的是retrofit2的2.0.0-beta4版本贞言,api調(diào)用有很大的變化,詳情請參考官方文檔阀蒂。Robolectic支持發(fā)送真實(shí)的網(wǎng)絡(luò)請求该窗,通過對響應(yīng)結(jié)果進(jìn)行測試,可大大的提升我們與服務(wù)端的聯(lián)調(diào)效率蚤霞。
以github api為例酗失,網(wǎng)絡(luò)請求的代碼如下:
public interface GithubService {
String BASE_URL = "https://api.github.com/";
@GET("users/{username}/repos")
Call<List<Repository>> publicRepositories(@Path("username") String username);
@GET("users/{username}/following")
Call<List<User>> followingUser(@Path("username") String username);
@GET("users/{username}")
Call<User> user(@Path("username") String username);
class Factory {
public static GithubService create() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
return retrofit.create(GithubService.class);
}
}
}
1. 測試真實(shí)的網(wǎng)絡(luò)請求
@Test
public void publicRepositories() throws IOException {
Call<List<Repository>> call = githubService.publicRepositories("geniusmart");
Response<List<Repository>> execute = call.execute();
List<Repository> list = execute.body();
//可輸出完整的響應(yīng)結(jié)果,幫助我們調(diào)試代碼
Log.i(TAG,new Gson().toJson(list));
assertTrue(list.size()>0);
assertNotNull(list.get(0).name);
}
這類測試的意義在于:
- (1)檢驗(yàn)網(wǎng)絡(luò)接口的穩(wěn)定性
- (2)檢驗(yàn)部分響應(yīng)結(jié)果數(shù)據(jù)的完整性(如非空驗(yàn)證)
- (3)方便開發(fā)階段的聯(lián)調(diào)(通過UT聯(lián)調(diào)的效率遠(yuǎn)高于run app后聯(lián)調(diào))
2. 模擬網(wǎng)絡(luò)請求
對于網(wǎng)絡(luò)請求的測試昧绣,我們需要知道確切的響應(yīng)結(jié)果值级零,才可進(jìn)行一系列相關(guān)的業(yè)務(wù)功能的斷言(比如請求成功/失敗后的異步回調(diào)函數(shù)里的邏輯),而發(fā)送真實(shí)的網(wǎng)絡(luò)請求時滞乙,其返回結(jié)果往往是不可控的,因此對網(wǎng)絡(luò)請求和響應(yīng)結(jié)果進(jìn)行模擬顯得特別必要鉴嗤。
那么如何模擬斩启?其原理很簡單,okhttp提供了攔截器 Interceptors ,通過該api醉锅,我們可以攔截網(wǎng)絡(luò)請求兔簇,根據(jù)請求路徑,不進(jìn)行請求的發(fā)送硬耍,而直接返回我們自定義好的相應(yīng)的response json字符串垄琐。
首先,自定義Interceptors的代碼如下:
public class MockInterceptor implements Interceptor {
@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
String responseString = createResponseBody(chain);
Response response = new Response.Builder()
.code(200)
.message(responseString)
.request(chain.request())
.protocol(Protocol.HTTP_1_0)
.body(ResponseBody.create(MediaType.parse("application/json"), responseString.getBytes()))
.addHeader("content-type", "application/json")
.build();
return response;
}
/**
* 讀文件獲取json字符串经柴,生成ResponseBody
*
* @param chain
* @return
*/
private String createResponseBody(Chain chain) {
String responseString = null;
HttpUrl uri = chain.request().url();
String path = uri.url().getPath();
if (path.matches("^(/users/)+[^/]*+(/repos)$")) {//匹配/users/{username}/repos
responseString = getResponseString("users_repos.json");
} else if (path.matches("^(/users/)+[^/]+(/following)$")) {//匹配/users/{username}/following
responseString = getResponseString("users_following.json");
} else if (path.matches("^(/users/)+[^/]*+$")) {//匹配/users/{username}
responseString = getResponseString("users.json");
}
return responseString;
}
}
相應(yīng)的resonse json的文件可以存放在test/resources/json/下狸窘,如下圖
再次,定義Http Client,并添加攔截器:
//獲取測試json文件地址
jsonFullPath = getClass().getResource(JSON_ROOT_PATH).toURI().getPath();
//定義Http Client,并添加攔截器
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(new MockInterceptor(jsonFullPath))
.build();
//設(shè)置Http Client
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(GithubService.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build();
mockGithubService = retrofit.create(GithubService.class);
最后坯认,就可以使用mockGithubService進(jìn)行隨心所欲的斷言了:
@Test
public void mockPublicRepositories() throws Exception {
Response<List<Repository>> repositoryResponse = mockGithubService.publicRepositories("geniusmart").execute();
assertEquals(repositoryResponse.body().get(5).name, "LoveUT");
}
這種做法不僅僅可以在寫UT的過程中使用翻擒,在開發(fā)過程中也可以使用氓涣,當(dāng)服務(wù)端的接口開發(fā)滯后于客戶端的進(jìn)度時,可以先約定好數(shù)據(jù)格式陋气,客戶端采用模擬網(wǎng)絡(luò)請求的方式進(jìn)行開發(fā)劳吠,此時兩個端可以做到不互相依賴。
3. 網(wǎng)絡(luò)請求的異步回調(diào)如何進(jìn)行測試
關(guān)于網(wǎng)絡(luò)請求之后的回調(diào)函數(shù)如何測試巩趁,筆者暫時也沒有什么自己覺得滿意的解決方案痒玩,這里提供一種做法,權(quán)當(dāng)拋磚引玉议慰,希望有此經(jīng)驗(yàn)的人提供更多的思路蠢古。
由于網(wǎng)絡(luò)請求和回調(diào)函數(shù)是在子線程和UI主線程兩個線程中進(jìn)行的,且后者要等待前者執(zhí)行完畢褒脯,這種情況要在一個TestCase中測試并不容易便瑟。因此我們要做的就是想辦法讓兩件事情同步的在一個TestCase中執(zhí)行,類似于這樣的代碼:
//此為Retrofit2的新api番川,代表同步執(zhí)行
//異步執(zhí)行的api為githubService.followingUser("geniusmart").enqueue(callback);
githubService.publicRepositories("geniusmart").execute();
callback.onResponse(call,response);
//對執(zhí)行回調(diào)后影響的數(shù)據(jù)做斷言
some assert...
這里我列舉一個場景到涂,并進(jìn)行相應(yīng)的單元測試:一個Activity中有個ListView,經(jīng)過網(wǎng)絡(luò)請求后颁督,在異步回調(diào)函數(shù)里加載ListView的數(shù)據(jù)践啄,點(diǎn)擊每一個item后,吐司其對應(yīng)的標(biāo)題沉御。
public class CallbackActivity extends Activity {
//省略一些全局變量聲明的代碼
/**
* 定義一個全局的callback對象屿讽,并暴露出get方法供UT調(diào)用
*/
private Callback<List<User>> callback;
@Override
protected void onCreate(Bundle savedInstanceState) {
//省略一些初始化UI組件的代碼
listView.setOnItemClickListener(new AdapterView.OnItemClickListener(){
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Toast.makeText(CallbackActivity.this,datas.get(position),Toast.LENGTH_SHORT).show();
}
});
//加載數(shù)據(jù)
loadData();
}
public void loadData() {
progressBar.setVisibility(View.VISIBLE);
datas = new ArrayList<>();
//初始化回調(diào)函數(shù)對象
callback = new Callback<List<User>>() {
@Override
public void onResponse(Call<List<User>> call, Response<List<User>> response) {
for(User user : response.body()){
datas.add(user.login);
}
ArrayAdapter<String> adapter = new ArrayAdapter<>(CallbackActivity.this,
android.R.layout.simple_list_item_1, datas);
listView.setAdapter(adapter);
progressBar.setVisibility(View.GONE);
}
@Override
public void onFailure(Call<List<User>> call, Throwable t) {
progressBar.setVisibility(View.GONE);
}
};
GithubService githubService = GithubService.Factory.create();
githubService.followingUser("geniusmart").enqueue(callback);
}
public Callback<List<User>> getCallback(){
return callback;
}
}
相應(yīng)的測試代碼如下:
@Test
public void callback() throws IOException {
CallbackActivity callbackActivity = Robolectric.setupActivity(CallbackActivity.class);
ListView listView = (ListView) callbackActivity.findViewById(R.id.listView);
Response<List<User>> users = mockGithubService.followingUser("geniusmart").execute();
//結(jié)合模擬的響應(yīng)數(shù)據(jù),執(zhí)行回調(diào)函數(shù)
callbackActivity.getCallback().onResponse(null, users);
ListAdapter listAdapter = listView.getAdapter();
//對ListView的item進(jìn)行斷言
assertEquals(listAdapter.getItem(0).toString(), "JakeWharton");
assertEquals(listAdapter.getItem(1).toString(), "Trinea");
ShadowListView shadowListView = Shadows.shadowOf(listView);
//測試點(diǎn)擊ListView的第3~5個Item后吠裆,吐司的文本
shadowListView.performItemClick(2);
assertEquals(ShadowToast.getTextOfLatestToast(), "daimajia");
shadowListView.performItemClick(3);
assertEquals(ShadowToast.getTextOfLatestToast(), "liaohuqiu");
shadowListView.performItemClick(4);
assertEquals(ShadowToast.getTextOfLatestToast(), "stormzhang");
}
這樣做的話要改變一些編碼習(xí)慣伐谈,比如回調(diào)函數(shù)不能寫成匿名內(nèi)部類對象,需要定義一個全局變量试疙,并破壞其封裝性诵棵,即提供一個get方法,供UT調(diào)用祝旷。
注:經(jīng)過后續(xù)研究履澳,使用Mockito的Capture才是解決異步測試的最佳方案,后面考慮出專門文章來說明怀跛。
四 數(shù)據(jù)庫篇
Robolectric從2.2開始距贷,就已經(jīng)可以對真正的DB進(jìn)行測試,從3.0開始測試DB變得更加便利吻谋,通過UT來調(diào)試DB簡直不能更爽忠蝗。這一節(jié)將介紹不使用任何框架的DB測試,ORMLite測試以及ContentProvider測試漓拾。
1. 不使用任何框架的DB測試(SQLiteOpenHelper)
如果沒有使用框架什湘,采用Android的SQLiteOpenHelper對數(shù)據(jù)庫進(jìn)行操作长赞,通常我們會封裝好各個Dao,并實(shí)例化一個SQLiteOpenHelper的單例對象闽撤,測試代碼如下:
@Test
public void query(){
AccountDao.save(AccountUtil.createAccount("3"));
AccountDao.save(AccountUtil.createAccount("4"));
AccountDao.save(AccountUtil.createAccount("5"));
AccountDao.save(AccountUtil.createAccount("5"));
List<Account> accountList = AccountDao.query();
assertEquals(accountList.size(), 3);
}
另外有一點(diǎn)要注意的是得哆,當(dāng)我們測試多個test時,會拋出一個類似于這樣的異常:
java.lang.RuntimeException: java.lang.IllegalStateException: Illegal connection pointer 37. Current pointers for thread Thread[pool-1-thread-1,5,main] []
解決方式便是每次執(zhí)行一個test之后哟旗,就將SQLiteOpenHelper實(shí)例對象重置為null贩据,如下:
@After
public void tearDown(){
AccountUtil.resetSingleton(AccountDBHelper.class, "mAccountDBHelper");
}
public static void resetSingleton(Class clazz, String fieldName) {
Field instance;
try {
instance = clazz.getDeclaredField(fieldName);
instance.setAccessible(true);
instance.set(null, null);
} catch (Exception e) {
throw new RuntimeException();
}
}
2. OrmLite測試
使用OrmLite對數(shù)據(jù)操作的測試與上述方法并無區(qū)別,同樣也要注意每次測試完后闸餐,要重置OrmLiteSqliteOpenHelper實(shí)例饱亮。
@After
public void tearDown(){
DatabaseHelper.releaseHelper();
}
@Test
public void save() throws SQLException {
long millis = System.currentTimeMillis();
dao.create(new SimpleData(millis));
dao.create(new SimpleData(millis + 1));
dao.create(new SimpleData(millis + 2));
assertEquals(dao.countOf(), 3);
List<SimpleData> simpleDatas = dao.queryForAll();
assertEquals(simpleDatas.get(0).millis, millis);
assertEquals(simpleDatas.get(1).string, ((millis + 1) % 1000) + "ms");
assertEquals(simpleDatas.get(2).millis, millis + 2);
}
3. ContentProvider測試
一旦你的App里有ContentProvider,此時配備完善和嚴(yán)謹(jǐn)?shù)膯卧獪y試用例是非常有必要的舍沙,畢竟你的ContentProvider是對外提供使用的近上,一定要保證代碼的質(zhì)量和穩(wěn)定性。
對ContentProvider的測試拂铡,需要借助影子對象ShadowContentResolver壹无,關(guān)于Shadow,我在上文中已經(jīng)有介紹過感帅,此處的Shadow可以豐富ContentResolver的行為斗锭,幫助我們進(jìn)行測試,代碼如下:
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class AccountProviderTest {
private ContentResolver mContentResolver;
private ShadowContentResolver mShadowContentResolver;
private AccountProvider mProvider;
private String AUTHORITY = "com.geniusmart.loveut.AccountProvider";
private Uri URI_PERSONAL_INFO = Uri.parse("content://" + AUTHORITY + "/" + AccountTable.TABLE_NAME);
@Before
public void setUp() {
ShadowLog.stream = System.out;
mProvider = new AccountProvider();
mContentResolver = RuntimeEnvironment.application.getContentResolver();
//創(chuàng)建ContentResolver的Shadow對象
mShadowContentResolver = Shadows.shadowOf(mContentResolver);
mProvider.onCreate();
//注冊ContentProvider對象和對應(yīng)的AUTHORITY
ShadowContentResolver.registerProvider(AUTHORITY, mProvider);
}
@After
public void tearDown() {
AccountUtil.resetSingleton(AccountDBHelper.class, "mAccountDBHelper");
}
@Test
public void query() {
ContentValues contentValues1 = AccountUtil.getContentValues("1");
ContentValues contentValues2 = AccountUtil.getContentValues("2");
mShadowContentResolver.insert(URI_PERSONAL_INFO, contentValues1);
mShadowContentResolver.insert(URI_PERSONAL_INFO, contentValues2);
//查詢所有數(shù)據(jù)
Cursor cursor1 = mShadowContentResolver.query(URI_PERSONAL_INFO, null, null, null, null);
assertEquals(cursor1.getCount(), 2);
//查詢id為2的數(shù)據(jù)
Uri uri = ContentUris.withAppendedId(URI_PERSONAL_INFO, 2);
Cursor cursor2 = mShadowContentResolver.query(uri, null, null, null, null);
assertEquals(cursor2.getCount(), 1);
}
@Test
public void queryNoMatch() {
Uri noMathchUri = Uri.parse("content://com.geniusmart.loveut.AccountProvider/tabel/");
Cursor cursor = mShadowContentResolver.query(noMathchUri, null, null, null, null);
assertNull(cursor);
}
@Test
public void insert() {
ContentValues contentValues1 = AccountUtil.getContentValues("1");
mShadowContentResolver.insert(URI_PERSONAL_INFO, contentValues1);
Cursor cursor = mShadowContentResolver.query(URI_PERSONAL_INFO, null, AccountTable.ACCOUNT_ID + "=?", new String[]{"1"}, null);
assertEquals(cursor.getCount(), 1);
cursor.close();
}
@Test
public void update() {
ContentValues contentValues = AccountUtil.getContentValues("2");
Uri uri = mShadowContentResolver.insert(URI_PERSONAL_INFO, contentValues);
contentValues.put(AccountTable.ACCOUNT_NAME, "geniusmart_update");
int update = mShadowContentResolver.update(uri, contentValues, null, null);
assertEquals(update, 1);
Cursor cursor = mShadowContentResolver.query(URI_PERSONAL_INFO, null, AccountTable.ACCOUNT_ID + "=?", new String[]{"2"}, null);
cursor.moveToFirst();
String accountName = cursor.getString(cursor.getColumnIndex(AccountTable.ACCOUNT_NAME));
assertEquals(accountName, "geniusmart_update");
cursor.close();
}
@Test
public void delete() {
try {
mShadowContentResolver.delete(URI_PERSONAL_INFO, null, null);
fail("Exception not thrown");
} catch (Exception e) {
assertEquals(e.getMessage(), "Delete not supported");
}
}
}
五 Love UT
寫UT是一種非常好的編程習(xí)慣失球,但是UT雖好岖是,切忌貪杯,作為一名技術(shù)領(lǐng)導(dǎo)者实苞,切忌拿測試覆蓋率作為指標(biāo)豺撑,如此一來會滋生開發(fā)者的抵觸心理,導(dǎo)致亂寫一通黔牵。作為開發(fā)者前硫,應(yīng)該時刻思考什么才是有價值的UT,什么邏輯沒必要寫(比如set和get)荧止,這樣才不會疲于奔命且覺得乏味。其實(shí)很多事情都是因果關(guān)系阶剑,開發(fā)人員不寫跃巡,所以leader強(qiáng)制寫,而leader強(qiáng)制寫牧愁,開發(fā)人員會抵觸而亂寫素邪。所以,讓各自做好猪半,一起來享受UT帶來的高質(zhì)量的代碼以及為了可測試而去思考代碼設(shè)計(jì)的編程樂趣兔朦。
本文的所有代碼仍然放在LoveUT這個工程里:
https://github.com/geniusmart/LoveUT
參考文章
http://square.github.io/retrofit/
https://github.com/square/okhttp/wiki/Interceptors
http://stackoverflow.com/questions/17544751/square-retrofit-server-mock-for-testing
https://github.com/robolectric/robolectric/issues/1890
最后偷线,行此文時,悲痛欲絕沽甥,越長大越不會表達(dá)自己的情感声邦,此文送給肚中遠(yuǎn)去的小小猴子,此生無緣摆舟。無論你在哪個時空亥曹,作為一個技術(shù)從業(yè)者,將保持純良恨诱,求真媳瞪,但行好事,希望能帶給你幸運(yùn)照宝。愿此坎之后蛇受,此生無坎。