Integrated Component Testとは
外部サービス(Web API等)とIntegrationされたUI Componentに着目し、テストすることです。
(どこかに定義があるわけではなく、僕が勝手に呼んでいる名前です。)
React Native公式でTestに関する説明があります。その中ではEnd-to-End Testsに分類されると思いますが、
ユーザシナリオに基づいてテストはしません。IntegrateされたComponentに着目し、テストを分割、実行していきます。
なぜ、Integrated Component Testしたいか
理由は以下の通りです。
1. デグレチェック/リファクタリングするためのテストがほしい
React NativeでAndroidアプリを開発しています。
そこそこ機能が増えてきて、
コミットする度にデグレしてないか気になるし、
自信をもってどんどんリファクタリングしていきたい。
テストがほしいと思いました。
2. 少ない努力で効果が得られるシンプルなテストにしたい
React Nativeから外部サービスと連携するロジックを切り離し、Unitテストを書く方法もあります。
むしろ、そうするべきです。
でも今のチームは開発メンバーも多くないですし、
とりあえず作る、リファクタリングするの順で進めたくなることが多いです。
テストはvalueとeffortを天秤にかけろとよく言われます。
そう、僕らは少ないeffortである程度のvalueを得たいのです。
じゃあどうするか、ある程度大きな塊Componentでテストするアプローチをとることを決めました。
3. mockする手間をはぶきたい
僕らのチームはフロントエンド/バックエンド両方を開発しています。
アプリからコールするWeb APIも自分たちで開発したものです。
つまり、両方のコードが手元にある。
ローカルでWeb APIサーバを起動させて、連携させることだってできます。
そんな状況だから、mockするのが手間に感じます。
mockを書くのはやめました。
やってみよう
ここからは実際にIntegrated Component Testを実行する方法を書きます。
テストにはe2eテストフレームワークappiumを利用します。
コードはこちらにあります。
1. 環境構築
React Nativeプロジェクト作成
mkdir IntegrationTestSample
cd IntegrationTestSample
npx react-native init IntegrationTestSampleApp --template react-native-template-typescript
cd IntegrationTestSample
npm install
npm run android
REST API 構築
JSON Serverを利用してRestful Web Serverを起動する。
vim server_db.json
ファイルに以下の内容をコピペする
{
"posts": [
{ "id": 1, "title": "json-server", "author": "typicode" }
],
"comments": [
{ "id": 1, "body": "some comment", "postId": 1 }
],
"profile": { "name": "typicode" }
}
以下のコマンドでサーバを起動する。privateIPにはご自身のPCにアサインされているIPをいれてください。
--hostを指定する理由はAxiosはAndroid Emulatorでlocalhostにアクセスできないためです。
yourPrivateIP=xxx.xxx.xxx.xxx
npx json-server --watch server_db.json --host ${yourPrivateIP}
以下の通り、コマンドを実行しレスポンスが返ってくれば、正常に動作しています。
curl http://${yourPrivateIP}:3000/posts
{
"id": 1,
"title": "json-server",
"author": "typicode"
}
]%
2. RESTと連携するアプリを準備する
npm install axios
vim App.tsx
App.tsxを開き、以下のの通りコピペする。
privateIPには先程と同じものを設定します。
import React, {useEffect, useState} from 'react';
import {
SafeAreaView,
StyleSheet,
View,
Text,
StatusBar,
TouchableOpacity,
Linking,
} from 'react-native';
const App = () => {
const [bookCard, setBookCard] = useState<BookCard>({
id: 0,
title: '',
author: '',
});
useEffect(() => {
// 起動時にREST Serverに格納されたデータをカードに反映する
const updateCard = async (): Promise<void> => {
const targetBook = await getDataFromServer();
if (targetBook) {
setBookCard({
id: targetBook.id,
title: targetBook.title,
author: targetBook.author,
});
}
};
updateCard();
}, []);
const styles = StyleSheet.create({
cardPostion: {
alignItems: 'center',
justifyContent: 'center',
height: '100%',
},
});
return (
<View accessibilityLabel="App">
<StatusBar barStyle="dark-content" />
<SafeAreaView>
<View style={styles.cardPostion}>
<Card
id={bookCard.id}
title={bookCard.title}
author={bookCard.author}
/>
</View>
</SafeAreaView>
</View>
);
};
type BookCard = {
id: number;
title: string;
author: string;
};
const Card: React.FC<BookCard> = ({title, author}) => {
const styles = StyleSheet.create({
container: {
width: '80%',
height: '10%',
borderWidth: 1,
borderRadius: 5,
justifyContent: 'center',
alignItems: 'center',
},
titleText: {
fontSize: 20,
},
authorText: {
fontSize: 15,
},
});
return (
<TouchableOpacity style={styles.container} accessibilityLabel="card">
<Text style={styles.titleText} accessibilityLabel="card_title">
{title}
</Text>
<Text style={styles.authorText} accessibilityLabel="card_author">
by {author}
</Text>
</TouchableOpacity>
);
};
/** ここからはREST Clientロジック **/
import axios from 'axios';
const privateIP = 'xxx.xxx.xxx.xxx'; // ***** ここにprivate ipを設定してください ***** //
const request = axios.create({
baseURL: `http://${privateIP}:3000`,
responseType: 'json',
});
// REST Serverからデータを取得する関数
const getDataFromServer = async (): Promise<BookCard | undefined> => {
const res = await request.get('/posts');
if (res.data) {
return res.data[0] as BookCard;
}
return undefined;
};
export default App;
Androidエミュレータを起動し、
npm run android
以下のように表示されれば、正しく動作しています。
3. テストの準備と実行
appiumのセットアップ
npm install -D appium wd
mkdir integratedComponentTest
vim integratedComponentTest/config.js
config.jsを作成します。以下の通り記述してください。
import wd from 'wd';
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
const PORT = 4723;
export const config = {
platformName: 'Android',
deviceName: 'Android Emulator',
app:
process.env.APK_PATH ||
'./android/app/build/outputs/apk/debug/app-debug.apk',
unicodeKeyboard: true,
resetKeyboard: true,
};
export const driver = wd.promiseChainRemote('localhost', PORT);
テストコードを書く
vim integratedComponentTest/sample.test.ts
import {config, driver} from './config';
beforeAll(async () => {
// アプリの起動処理
await driver.init(config);
// 起動時間の分スリープを入れる
await driver.sleep(5000);
});
describe('Integrated Component Test', () => {
describe('Card Component', () => {
beforeAll(async () => {
// Given: card component is available
expect(await driver.hasElementByAccessibilityId('card')).toBe(true);
});
test('title should be json_server ', async () => {
// Then
expect(await driver.elementByAccessibilityId('card_title').text()).toBe(
'json-server',
);
});
test('author should be typicode', async () => {
// Then
expect(await driver.elementByAccessibilityId('card_author').text()).toBe(
'by typicode',
);
});
});
});
テスト実行
事前にappiumを起動しておきます。
npx appium
以下の通り、テストを実行します。
➜ npm test sample.test.ts
> integration-sample-app@0.0.1 test
> jest --verbose "sample.test.ts"
PASS integratedComponentTest/sample.test.ts (14.084s)
Integrated Component Test
Card Component
✓ title should be json_server (110ms)
✓ author should be typicode (646ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 14.124s
Ran all test suites matching /sample.test.ts/i.
まとめ
Integrated Component Testする方法を書きました。
技術スタックとしては一般的なe2e testと同じですが、思想は違います。
IntegrateされたComponentに着目し、コンポーネント毎にテストを実行しています。
正直、e2e testフレームワークを利用することは理想ではありません。
mockせずにReact Native Testing Library等を使ってintegration testテストかけたら最高ですが、
私が試した限りではそれはできないようなのです。
エミュレータを起動せずにテストする方法を知っている方がいらっしゃいましたらお教えいただけますと幸いです。
今回は簡単なサンプルであり、実際のアプリに組み込むには物足りない内容だったかもしれません。
次回、ディープリンク等も利用したテスト方法について書きたいと思います。