Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

結合テストを導入 #697

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/ci-playwright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: CI / Playwright
on: [push]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- uses: actions/setup-node@v3
with:
node-version: 16
cache: 'yarn'

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Install Playwright Browsers
run: yarn playwright install chromium webkit --with-deps

- name: Run Playwright tests
run: yarn test:integration

- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
CI: true

- name: Test
run: yarn test --ci --coverage --maxWorkers=2
run: yarn test:unit --ci --coverage --maxWorkers=2
env:
CI: true

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ dist-ssr
*.local
tsconfig.staged.json
storybook-static
/test-results/
/playwright-report/
/playwright/.cache/
45 changes: 45 additions & 0 deletions docs/integration-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Integration Tests Guide

Twin:te Front では [Playwright](https://playwright.dev/) を利用した結合テストを実施しています。

## 実行

ブラウザバイナリなどの依存をインストール

```console
yarn playwright install --with-deps
```

テストを実行

```console
yarn test:integration # ヘッドレスモードで実行
yarn test:integration --headed # ブラウザを表示して実行
yarn test:integration --ui # 専用の GUI で実行
```

## 方針

このテストでは最低限のユーザの行動を実行できることを保証します。
例えば以下のような内容です。

- 授業を検索したら授業結果が表示される
- 検索結果でチェックボックスを押すと授業が選択され、追加ボタンがアクティブになり押せる

反対に以下のような網羅性の高さは保証しません。

- 授業検索でそれぞれの時限指定の入力パターンで正しく表示されるか
- スマホ、タブレット、PC のそれぞれで正しく表示されるか
- Twin:te は 8, 9 割がスマホでの利用なので結合テストでは基本的にスマホのみを対象とします

## 背景

Twin:te Front 過去に重大な不具合が長期間放置されていたことがあります。

- アーキテクチャの以降時にバグが混入し、KdB もどきからのインポートが 3 ヶ月間ほどできない状態で放置されていた
- パッケージアップグレード時にライブラリの仕様が変わり、一部のトグルボタンが半年ほど機能しない状態で放置されていた

これらの不具合は QA をリリース前に行っていれば防げたものである一方、安定した開発リソースが無い Twin:te ではリリース前に毎回 QA を行うことは難しいです。
そこで QA の一部を代替するために結合テストを導入しました。

詳細は [https://github.com/twin-te/twinte-front/issues/689] を参照してください。
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
"typecheck": "vue-tsc --noEmit",
"build": "yarn typecheck && vite build",
"build:staging": "vite build --mode staging",
"build:production": "yarn typecheck && SOURCE_MAP=1 vite build",
"build:production": "yarn typecheck && VITE_ENABLE_SENTRY=true VITE_ENABLE_GA=true SOURCE_MAP=1 vite build",
"preview": "yarn build && vite preview --port 8080",
"format": "prettier ./src --check",
"format:fix": "prettier ./src --write",
"lint": "eslint --ext .ts,.tsx,.vue ./src",
"lint:fix": "yarn lint --fix",
"test": "jest",
"test:unit": "jest",
"test:integration": "playwright test",
"apigen": "rm -rf src/api && npx openapi2aspida",
"prepare": "husky install && rimraf ./node_modules/@types/react",
"storybook": "start-storybook -p 6006",
Expand Down Expand Up @@ -43,6 +44,7 @@
"vuex": "4.1.0"
},
"devDependencies": {
"@playwright/test": "^1.33.0",
"@rushstack/eslint-patch": "^1.1.1",
"@storybook/addon-actions": "^6.5.12",
"@storybook/addon-essentials": "^6.4.19",
Expand Down
34 changes: 34 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { defineConfig, devices } from '@playwright/test';

/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? "100%" : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:8080',
trace: 'on-first-retry',
},
projects: [
// Twin:te ユーザの大半はモバイルからの利用なので、モバイルに絞ってテストを書く
// PC のレイアウトも考慮すると要素の表示・非表示など考慮することが多くなるため
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
],
webServer: {
command: 'VITE_API_URL=http://localhost:8080/api/v3 yarn preview',
url: 'http://localhost:8080',
reuseExistingServer: !process.env.CI,
},
});
8 changes: 4 additions & 4 deletions src/ui/components/ToggleButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ export default defineComponent({
'toggle-button': true,
}"
>
<div class="toggle-button__label" @click="handleChange('left', $event)">
<button class="toggle-button__label" @click="handleChange('left', $event)">
{{ labels.left }}
</div>
<div class="toggle-button__label" @click="handleChange('right', $event)">
</button>
<button class="toggle-button__label" @click="handleChange('right', $event)">
{{ labels.right }}
</div>
</button>
<div
:class="{
'toggle-button__slider': true,
Expand Down
3 changes: 2 additions & 1 deletion src/ui/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Sentry.init({
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
logErrors: true,
enabled: import.meta.env.VITE_ENABLE_SENTRY === "true",
});

const head = createHead();
Expand All @@ -38,7 +39,7 @@ app
createGtm({
id: "GTM-PHSLD8B",
vueRouter: router,
enabled: import.meta.env.PROD,
enabled: import.meta.env.VITE_ENABLE_GA === "true",
debug: import.meta.env.DEV,
})
)
Expand Down
62 changes: 62 additions & 0 deletions tests/mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Page } from "@playwright/test";

const API_BASE_URL = `http://localhost:8080/api/v3`;

type Method = "GET" | "POST" | "PUT" | "DELETE";

// route.fulfill function's parameter type
type fullfillOptions = Parameters<
Parameters<Parameters<Page["route"]>[1]>[0]["fulfill"]
>[0];

export const mockApi = async (
page: Page,
path: string | RegExp,
responses: {
[K in Method]?: fullfillOptions;
},
): Promise<void> => {
const routeTarget = typeof path === "string"
? `${API_BASE_URL}${path}`
: (url: URL) =>
url.href.startsWith(API_BASE_URL) && path.test(url.pathname);
page.route(routeTarget, async (route) => {
for (const [method, response] of Object.entries(responses)) {
if (route.request().method() === method) await route.fulfill(response);
}
});
};

export const USER_ID = `00000000-0000-0000-0000-000000000000`;
export const DUMMY_COURSE = {
"recommendedGrades": [2],
"id": "00000000-0000-0000-0000-000000000000",
"year": 2023,
"code": "XX000000",
"name": "ダミー講義名",
"instructor": "ダミー講義の講師",
"credit": 2,
"overview": "ダミー講義の説明文",
"remarks": "対面",
"lastUpdate": "2023-03-28T01:21:32.000Z",
"hasParseError": false,
"isAnnual": false,
"methods": ["Asynchronous"],
"schedules": [
{ "module": "SpringA", "day": "Mon", "period": 5, "room": "" },
{ "module": "SpringA", "day": "Mon", "period": 6, "room": "" },
{ "module": "SpringA", "day": "Mon", "period": 5, "room": "" },
{ "module": "SpringA", "day": "Mon", "period": 6, "room": "" },
],
};
export const DUMMY_REGISTERED_COURSE = {
"tags": [],
"id": "00000000-0000-0000-0000-000000000000",
"userId": "00000000-0000-0000-0000-000000000000",
"year": 2023,
"memo": "",
"attendance": 0,
"absence": 0,
"late": 0,
"course": DUMMY_COURSE,
};
77 changes: 77 additions & 0 deletions tests/search.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { expect, test } from "@playwright/test";
import {
DUMMY_COURSE,
DUMMY_REGISTERED_COURSE,
mockApi,
USER_ID,
} from "./mock";

test.beforeEach(async ({ page }) => {
await mockApi(page, "/users/me", {
"GET": { json: { id: USER_ID } },
});
await mockApi(page, /events/, {
"GET": { json: [] },
});
await mockApi(page, /information/, {
"GET": { json: [] },
});
await mockApi(page, /school-calendar\/modules/, {
"GET": {
json: [
{
"id": 1,
"year": 2023,
"module": "SpringA",
"start": "2023-04-05T00:00:00.000Z",
"end": "2024-03-31T00:00:00.000Z",
},
],
},
});
await mockApi(page, "/courses/search", {
"POST": { json: [DUMMY_COURSE] },
});
await mockApi(page, /registered-courses/, {
"GET": { json: [] },
});
await mockApi(page, "/registered-courses", {
"POST": { json: [DUMMY_REGISTERED_COURSE] },
});
});

test.describe("授業の検索", () => {
test("検索して追加できる", async ({ page }) => {
await page.goto("/add/search");

// 検索
await page.getByPlaceholder("例)情報 倫理").fill("ダミー講義名");
await page.getByRole("button").filter({ hasText: "search" }).click();
await expect(page.locator(".search__result")).toContainText("ダミー講義名");

// 選択して追加
await page.getByText("done").click();
await page.getByRole("button").filter({ hasText: "選択した授業を追加" }).click();
await expect(page).toHaveTitle("Twin:te | ホーム");
await expect(page.locator(".table")).toContainText("ダミー講義名");
});

test("検索結果の詳細と簡易を切り替えられる", async ({ page }) => {
Copy link
Member Author

@HosokawaR HosokawaR May 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Twin:te ではソースコード中のコメントなどが英語で書かれていますが、テストではなるべく日本語で書きたいと思います。理由は以下です。

  • 結合テストは UI と結びつきが強いので、UI で使われている言語と同じ言語を使うほうが便利
    • 例えば「詳細」「簡易」などは「詳細」「簡易」と書かれたボタンがあるのでわかりやすいですが、これを英語にすると UI のどの部分を指しているのかがわかりにくくなります
  • テストにおいてはテスト名やコメントが重要になるので、英語の時制や主体の間違いで大きく影響がでてしまうリスクがあるので日本語で書きたい

await page.goto("/add/search");

// 検索
await page.getByPlaceholder("例)情報 倫理").fill("ダミー講義名");
await page.getByRole("button").filter({ hasText: "search" }).click();
await expect(page.locator(".search__result")).toContainText("ダミー講義名");

// 簡易モードで検索結果を表示
await page.getByRole("button").filter({ hasText: "簡易" }).click();
await expect(page.getByText("ダミー講義名")).toBeVisible();
await expect(page.getByText("ダミー講義の説明文")).not.toBeVisible();

// 詳細モードで検索結果を表示
await page.getByRole("button").filter({ hasText: "詳細" }).click();
await expect(page.getByText("ダミー講義名")).toBeVisible();
await expect(page.getByText("ダミー講義の説明文")).toBeVisible();
});
});
25 changes: 20 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2033,6 +2033,16 @@
tiny-glob "^0.2.9"
tslib "^2.4.0"

"@playwright/test@^1.33.0":
version "1.33.0"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.33.0.tgz#669ef859efb81b143dfc624eef99d1dd92a81b67"
integrity sha512-YunBa2mE7Hq4CfPkGzQRK916a4tuZoVx/EpLjeWlTVOnD4S2+fdaQZE0LJkbfhN5FTSKNLdcl7MoT5XB37bTkg==
dependencies:
"@types/node" "*"
playwright-core "1.33.0"
optionalDependencies:
fsevents "2.3.2"

"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
Expand Down Expand Up @@ -7121,6 +7131,11 @@ fs.realpath@^1.0.0:
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==

[email protected], fsevents@^2.1.2, fsevents@^2.3.2, fsevents@~2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==

fsevents@^1.2.7:
version "1.2.13"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38"
Expand All @@ -7129,11 +7144,6 @@ fsevents@^1.2.7:
bindings "^1.5.0"
nan "^2.12.1"

fsevents@^2.1.2, fsevents@^2.3.2, fsevents@~2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==

function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
Expand Down Expand Up @@ -10318,6 +10328,11 @@ pkg-dir@^5.0.0:
dependencies:
find-up "^5.0.0"

[email protected]:
version "1.33.0"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.33.0.tgz#269efe29a927cd6d144d05f3c2d2f72bd72447a1"
integrity sha512-aizyPE1Cj62vAECdph1iaMILpT0WUDCq3E6rW6I+dleSbBoGbktvJtzS6VHkZ4DKNEOG9qJpiom/ZxO+S15LAw==

[email protected]:
version "1.6.4"
resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz#c9711ac4dc48a685dabafc86f8b6dd9f8df84149"
Expand Down