Lesson 10
Vue のテスト
Lesson 10
Chapter 1
単体テスト
1. 単体テストとは
単体テストは、ユニットテスト(unit test)とも呼ばれ、プログラム作成後に比較的小さな単位(ユニット)で行うテストのことをいいます。
Vue における単体テストは、個々のコンポーネントやそのメソッドに対して、プログラムが期待どおりに動作しているかのテストを行うことになります。
本チャプターでは、Vitest(ビテスト/ヴィテスト)というツールを使用して、単体テストを学習していきます。
2. Vitest とは
Vitest は、Vite を利用した単体テストのフレームワークであり、Vue/Vite チームのメンバーによって開発・保守されています。
○ Vitest 公式ページ:https://vitest.dev/
Vitest は、Vite を念頭に置いて開発されたツールであることから、Vite で作成したプロジェクトとの相性がよく、処理速度が高速な点に特長があります。
なお、これまで、Vue CLI を使用した Vue プロジェクトの単体テストには Jest(※)などの JS フレームワークが利用されてきました。
しかし、Vite プロジェクトにおいては、上記の Jest との間に重複した処理が多く存在するなど、その相性があまり良くありません。
そのため、最小限の労力で Vite ベースのプロジェクトと統合できる Vitest を利用することが Vue 公式でも推奨されています。
Jest とは
Jest は、JavaScript において最も人気の高い単体テスト用フレームワークの 1 つであり、Meta 社(Facebook 社)により開発・保守されています。
なお、Vitest は、Jest と互換性のあるように設計されており、簡易に Jest から Vitest への移行ができるようになっています。
なお、Vue で Vitest を使用する場合は、Vue 公式の単体テスト用ライブラリである Vue Test Utils を使用して、コンポーネント操作などを行うことになります。
3. Vitest を追加した新規プロジェクトの作成
それでは、実際に Vitest を使用してみましょう。
ここでは、create-vue ライブラリを使用して、新しく Vite プロジェクトを作成します。
以下の手順でプロジェクトの作成を行ってください。
① 作業フォルダに移動
まず、PowerShell を開いて、プロジェクトを作成するフォルダに移動します。
PS C:\Users\username> cd C:\vue_lesson
PS C:\vue_lesson>
② Vite プロジェクト生成コマンドの実行
Vue プロジェクトを作成するコマンドを入力します。
PS C:\vue_lesson> npm init vue@3
③ プロジェクト名の指定
プロジェクト名、ここでは「vitest-sample」と入力して Enter キーを押します(他の名前でも OK です)。
④ 各選択肢について
ここでは「Add Vitest for Unit Testing?」の選択肢のみ「Yes」と選択します。
その他の選択肢は、全て「No」を選択します。
以上の状態になれば、プロジェクトの作成は完了です。
⑤ npm install の実行
プロジェクトが作成されたら vitest-sample
プロジェクトを VSCode で開き、ターミナルを開いて npm install
を実行します。
プロジェクトを作成する時期によって Warnig が出たりしますが、Error さえ出なければ OK としてください。
⑥ ローカルサーバを起動してブラウザで表示
npm run dev
コマンドを打ってサーバを起動しましょう。
以上のように表示されれば、新規プロジェクトの作成は完了です。
確認ができたら「ctrl + c」または「q」を入力して、サーバを停止しておいてください。
4. 自動生成されたコードを確認する
4-1. package.json ファイル
まず、package.json
ファイルから確認してみましょう。
① devDependencies に追加された内容
最初に、下の方の devDependencies
に記載されているライブラリから確認します。
Vitest を選択したことにより、以下の 3 つのライブラリが追加されています。
No | ライブラリ | 説明 |
---|---|---|
1 | Vue Test Utils | Vue 公式の単体テスト用ライブラリ |
2 | jsdom | Node.js 上で HTML の DOM 操作を実行できるライブラリ |
3 | Vitest | Vite を利用した単体テストフレームワーク |
上記のうち「Vue Test Utils」と「Vitest」は既に説明したとおりです。
No 2 の「jsdom」は、ブラウザを使用しなくとも、メモリ上に DOM を構築することができるライブラリです。
この jsdom を使用することで、ブラウザを立ち上げなくとも、コンポーネントを生成して DOM 操作を行うことができます。
② scripts に追加された内容
次に、scripts
のところを見てみましょう。
Vitest を選択したことにより、以下の 1 行が追加されています。
"test:unit": "vitest --environment jsdom --root src/"
これにより、npm run test:unit
とコマンドを打つことで、Vitest を実行することができます。
指定されているオプションは、次のような意味となります。
No | オプション | 説明 |
---|---|---|
1 | --environment |
テストに使用する環境を指定。 ここでは、jsdom 環境を使用してテストを実行する。 |
2 | --root |
プロジェクトルートを指定。 ルート内にある .spec.js または .test.js ファイルを実行する。
|
4-2. HelloWorld.spec.js ファイル
続いて、src\components\__tests__\HelloWorld.spec.js
ファイルを開いてください。
このファイルは、Vitest を選択したことにより自動生成されたものとなります。
コードは、次のようになっています。
src\components\__tests__\HelloWorld.spec.js
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import HelloWorld from '../HelloWorld.vue'
describe('HelloWorld', () => {
it('renders properly', () => {
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
expect(wrapper.text()).toContain('Hello Vitest')
})
})
Vitest から使用しているメソッドは、以下のとおりです。
これら 3 つが、最も使用頻度の高いメソッドとなります。
No | メソッド | 説明 |
---|---|---|
1 | describe | テストブロックを指定する。 |
2 | it(または test) | テスト内容を記述する。 |
3 | expect | テストの評価(値の検証)を行う。 |
各メソッドについて、以下 1 つずつ説明をしていきます。
まずは、おおよそのコードの流れを把握していただければと思います。
① it メソッド
it
メソッドは、テスト内容を記述するメソッドとなります。
it('テスト内容の説明', () => {
// テスト内容を記述
})
第 1 引数に文字列で「テスト内容の説明」を記載し、第 2 引数にコールバック関数で「テストの内容」を記述します。
HelloWorld.spec.js
を見ると、次のように指定しています。
it('renders properly', () => {
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
expect(wrapper.text()).toContain('Hello Vitest')
})
第 1 引数の 'renders properly'
は「レンダリングが適切か」という説明文です。
第 2 引数では、まず、mount
メソッドで HelloWorld コンポーネントをマウントしています。その際に
props
として { msg: 'Hello Vitest' }
を渡しています。
mount
メソッドの戻り値は、マウントされたコンポーネントと仮想 DOM を含む Wrapper(ラッパー)です。
次の行で、expect
メソッドでテスト結果の評価を行っています(詳細は後述します)。
なお、it
メソッドの別名として test
メソッドを使用することもできます。名称が異なるだけで実体は同じであるため、以下のように書いても全く同じ結果となります。
test('renders properly', () => {
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
expect(wrapper.text()).toContain('Hello Vitest')
})
② expect メソッド
expect
メソッドは、テストの評価を行うメソッドとなります。
具体的に言えば、取得された値(算出された値)がテストの期待値に沿ったものかどうかの判定を行うこととなります。
expect(比較する値)
expect
メソッドは、Matcher(マッチャー)と呼ばれる「テストの評価条件を定義するメソッド」とともに使用します。
HelloWorld.spec.js
では、expect(文字列1).toContain(文字列2)
という形で toContain
という Matcher を使用しています。この Matcher は「文字列 1
に文字列 2
が含まれるか否か」を判定するものとなります。
expect(wrapper.text()).toContain('Hello Vitest')
expect
の引数にある wrapper.text()
は、text
メソッドを使用して wrapper
(ラッパー)からコンポーネントに表示される文字列を取得するものとなります。
つまり、ここで判定されるのは「コンポーネントで表示される文字列に、'Hello Vitest'
という文言が含まれるかどうか」ということになります。
③ describe メソッド
describe
メソッドは、テストブロックを指定するメソッドとなります。
describe('テストブロックの説明', () => {
// テストケースを記述
})
HelloWorld.spec.js
では、次のように指定しています。
describe('HelloWorld', () => {
it('renders properly', () => {
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
expect(wrapper.text()).toContain('Hello Vitest')
})
})
テストブロックの説明として 'HelloWorld'
と記述し、HelloWorld コンポーネント関連のテストであることを示しています。
この例では、一つのテストケースしか含まれていませんが、一般的には、複数のテストケースを記述することが多いです。
4-3. 自動生成コードでテスト実行
自動生成されたテストコードを使用して、テストの実行をしてみましょう。
以下のように npm run test:unit
とコマンドを打ち Enter キーを押してください。
次の赤枠部分のようなテスト結果が表示されるはずです。
Files 1 passed
は全部で 1 つのファイルのテストが成功したことを、Tests 1 passed
は全部で 1 つのテストが成功したことを表しています。
なお、テストについてもホットリロードが効いているため、コードの変更を保存するとテストの再実行が行われます。
末尾に「press h to show help, press q to quit
」と記載されています。
ここに書いてあるとおり、q キーを押すことでテストを終了することができます(ctrl + c でも終了できます)。
また、h キーを押すと、次のようなヒントが表示されます。
a キーを押すと全てのテストを再実行、f キーを押すと失敗したテストのみを再実行することができるなど、Vitest の使用方法が確認できます。
5. Matcher(マッチャー)
5-1. Matcher の種類
テスト結果の評価に使用する Matcher(マッチャー)には、主に以下のようなものがあります。その他の Matcher については公式ページを参照してください。
No | Matcher | 説明 |
---|---|---|
1 | not | 等しくない。 |
2 | toBe | 値が等しい(浮動小数点数には使用できない)。オブジェクトの参照が一致している。 |
3 | toBeCloseTo | 浮動小数点数が等しい。 |
4 | toBeDefined | 値が undefined でない。 |
5 | toBeUndefined | 値が undefined である。 |
6 | toBeTruthy | 値が true である。 |
7 | toBeFalsy | 値が false である。 |
8 | toBeNull | 値が null である。 |
9 | toBeGreaterThan | 指定した値より大きい。 |
10 | toBeGreaterThanOrEqual | 指定した値以上である。 |
11 | toBeLessThan | 指定した値より小さい。 |
12 | toBelessThanOrEqual | 指定した値以下である。 |
13 | toEqual | 値が等しい。オブジェクトの構造が一致する(undefined は無視)。 |
14 | toStrictEqual | オブジェクトの構造が一致する(undefined も含む)。 |
15 | toContain | 値が配列内にある。文字列が対象の文字列内に含まれる。 |
16 | toHaveLength | 長さ(length)が一致する。 |
17 | toHaveProperty | 指定したプロパティがオブジェクトに存在する。 |
18 | toMatch | 文字列が正規表現(または文字列)に一致する。 |
19 | toMatchObject | オブジェクトのサブセット(対象オブジェクトの一部分)が一致する。 |
20 | toThrowError | エラーがスローされている。 |
5-2. Matcher の具体例
実際に Matcher を使用してコードを書いてみましょう。
HelloWorld.spec.js
と同じフォルダに MatcherSample.spec.js
ファイルを作成してください。
次のように、ご自身でコードを記述してみてください。
コードをテキストで表示
src\components\__tests__\MatcherSample.spec.js
import { test, expect } from 'vitest'
test('not', () => {
expect(1).not.equal(2)
})
test('toBe', () => {
expect(100).toBe(100) // 値の一致
const obj = { bar: 0 }
expect(obj).toBe(obj) // 参照の一致
expect({ foo: 1 }).not.toBe({ foo: 1 }) // 参照が異なる(not で否定)
})
test('toBeCloseTo', () => {
expect(1.23).toBeCloseTo(1.23) // 浮動小数点数
})
test('toBeUndefined', () => {
expect().toBeUndefined()
})
test('toBeTruthy', () => {
expect(true).toBeTruthy()
expect(1).toBeTruthy() // 0 以外は Truthy
})
test('toBeFalsy', () => {
expect(false).toBeFalsy()
expect(0).toBeFalsy() // 0 は Falsy
})
test('toEqual', () => {
expect(100).toEqual(100)
expect({ foo: 1 }).toEqual({ foo: 1 }) // 参照は異なるが構造が一致
expect({ foo: 1, bar: undefined }).toEqual({ foo: 1 }) // undefined は無視
})
test('toStrictEqual', () => {
expect({ foo: 1, bar: undefined }).not.toStrictEqual({ foo: 1 }) // undefined も含めて判定
})
test('toContain', () => {
expect(['apple', 'banana', 'orange']).toContain('orange')
})
test('toHaveLength', () => {
expect(['apple', 'banana', 'orange']).toHaveLength(3)
})
test('toMatch', () => {
expect('hello world').toMatch(/ll.*ld/)
})
test('toMatchObject', () => {
expect({ foo: 1, bar: 2, baz: 9 }).toMatchObject({ bar: 2, baz: 9 })
})
test
メソッド(it
メソッドでも OK)を 12 個追加しました。describe
メソッドはあえて使用していません。
それぞれの Matcher の機能は、表と照らし合わせながら確認してみてください。
コードが書けたら npm run test:unit
コマンドでテストを実行してみましょう。
次のように全て成功(passed
)となれば OK です。
新しくファイルを 1 つ追加したので、ファイル数は 2 つ(Files 2 passed
)、また、テストを 12 個追加したので、合計のテスト数は 13 個(Tests 13 passed
)となっていることも確認できると思います。
6. Wrapper(ラッパー)
コンポーネントをマウントする際は、Vue Test Utils の mount
メソッドを使用します。
この mount
メソッドの戻り値として「マウントされたコンポーネントと仮想 DOM を含む Wrapper(ラッパー)」が返却されます。
6-1. Wrapper で使用できるメソッド
Wrapper に用意されているメソッドを使用することで、コンポーネントに関する情報を取得したり、コンポーネントの操作などを行うことができます。
Wrapper で使用できるメソッドには、主に以下のようなものがあります。
No | メソッド | 説明 |
---|---|---|
1 | attributes | DOM ノードの属性を返す。 |
2 | classes | 要素のクラスの配列を返す。 |
3 | emitted | 発行された全てのイベントを返す。 |
4 | exists | 要素が存在するか否かを返す。 |
5 | find |
最初に見つかった要素の Wrapper を返す。 指定方法は、JavaScript の querySelector() と同じ。 |
6 | findAll |
見つかった全ての要素の Wrapper 配列を返す。 ベースは querySelectorAll() と同じ。 |
7 | findComponent |
見つかったコンポーネントの Wrapper を返す。 |
8 | findAllComponents |
見つかった全てのコンポーネントの Wrapper 配列を返す。 |
9 | html | 要素の HTML を返す。 |
10 | isVisible | 要素が表示されているか否かを返す。 |
11 | props | コンポーネントに渡された props を返す。 |
12 | setData |
コンポーネントのプロパティを更新する。 ただし、setup() 関数では使用できない。 |
13 | setProps | コンポーネントの props を更新する。 |
14 | text | 要素のテキストを返す。 |
15 | trigger | DOM イベントを実行する。 |
その他のメソッドなど詳細は、公式ページを参照してください。
6-2. メソッドの具体例
それでは、Wrapper のメソッドを使用して実際にコードを書いてみましょう。
様々なメソッドを試せるように、まず、コンポーネントの追加と修正を行います。
① Foo.vue コンポーネントの追加
src\components
ディレクトリに、以下の Foo.vue
コンポーネントを追加してください。
コードをテキストで表示
src\components\Foo.vue
<script setup>
defineProps({
truthy: Boolean,
object: Object,
string: String
})
const emits = defineEmits(['emitTest'])
const emitTest = (text) => emits('emitTest', text)
emitTest('Hello')
emitTest('Goodbye')
</script>
<template>
<div id="foo-id" class="foo-class">
<h4>Fooコンポーネント</h4>
<p>{{ truthy }} / {{ object }} / {{ string }}</p>
<span v-show="false">isVisible のテスト!</span>
</div>
</template>
あくまでテストを実行するためのコンポーネントのため、内容に深い意味はありません。
② HelloWorld.vue コンポーネントの修正
次に、src\components\HelloWorld.vue
ファイルに、下図の赤枠部分を追加します。
コードをテキストで表示
src\components\HelloWorld.vue
<script setup>
import Foo from './Foo.vue'
defineProps({
msg: {
type: String,
required: true
}
})
</script>
<script>
export default {
data() {
return { count: 1 }
}
}
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
You’ve successfully created a project with
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
</h3>
</div>
<div>
<Foo truthy :object="{ bar: 'hoge' }" string="fuga" />
<div>Count: {{ count }}</div>
<button @click="count++">increment</button>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>
上図のとおり、Foo コンポーネントを使用できるように修正しています。
また、setData
メソッドが setup
関数内では実行できないことから、別途 <scpipt>
タグを追加して count
変数を定義しています(中央の赤枠部分)。
③ WrapperSample.spec.js ファイルの追加
HelloWorld.spec.js
と同じフォルダに WrapperSample.spec.js
ファイルを追加します。
以下のように、テストコードを記述してください。
コードをテキストで表示
src\components\Foo.vue
import { describe, test, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import HelloWorld from '../HelloWorld.vue'
import Foo from '../Foo.vue'
describe('Wrapperのメソッド', () => {
test('attributes: DOMノードの属性を取得', () => {
const wrapper = mount(Foo)
expect(wrapper.attributes('id')).toEqual('foo-id')
expect(wrapper.attributes('class')).toEqual('foo-class')
})
test('classes: 要素のクラスの配列を取得', () => {
const wrapper = mount(Foo)
expect(wrapper.classes()).toContain('foo-class')
})
test('emitted: emitイベントを取得', () => {
const wrapper = mount(Foo)
expect(wrapper.emitted().emitTest[0]).toContain('Hello') // 1回目のemit
expect(wrapper.emitted().emitTest[1]).toContain('Goodbye') // 2回目のemit
})
test('exists: 要素の存否を確認', () => {
const wrapper = mount(Foo)
expect(wrapper.find('#foo-id').exists()).toBe(true)
expect(wrapper.find('#bar-id').exists()).toBe(false) // 存在しないid
})
test('find: 要素を検索して取得', () => {
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
expect(wrapper.find('.green').text()).toEqual('Hello Vitest') // greenクラスを検索
})
test('findAll: 全ての要素を検索して配列で取得', () => {
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
const elements = wrapper.findAll('a') // aタグを検索
expect(elements).toHaveLength(2)
expect(elements[1].text()).toEqual('Vue 3')
})
test('getComponent: コンポーネントを検索して取得', () => {
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
const foo = wrapper.getComponent({ name: 'Foo' }) // Fooコンポーネントを検索
expect(foo.text()).toContain('Fooコンポーネント') // コンポーネント内の表示文字で確認
})
test('isVisible: 要素の表示の有無を確認', () => {
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
expect(wrapper.find('span').isVisible()).toBe(false)
})
test('props: コンポーネントのpropsを取得', () => {
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
const foo = wrapper.getComponent({ name: 'Foo' })
expect(foo.props('truthy')).toBe(true)
expect(foo.props('object')).toStrictEqual({ bar: 'hoge' })
expect(foo.props('string')).toBe('fuga')
})
test('setData: コンポーネントのプロパティを更新', async () => {
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
await wrapper.setData({ count: 1 }) // countプロパティは Options API で定義
expect(wrapper.text()).toContain('Count: 1')
})
test('setProps: コンポーネントのpropsを更新', async () => {
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
await wrapper.setProps({ msg: 'Goodbye' })
expect(wrapper.find('.green').text()).toEqual('Goodbye')
})
test('trigger: DOMイベントを実行', async () => {
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
await wrapper.find('button').trigger('click') // count の初期値 1 から 2 に変更
expect(wrapper.text()).toContain('Count: 2')
})
})
一つ一つのテストコードについては説明をしませんので、どのようにデータを取得/更新しているかを読み解きながらコードを書いてみてください。
コードが書けたら npm run test:unit
でテストを実行しましょう。
次のように表示されれば成功です。
以上、基本的な事項のみですが、Vitest を使用した単体テストの方法を学習しました。
他にも、様々な機能がありますので、必要に応じて公式ページなどを見ながら、理解を深めていただければと思います。

Lesson 10
Chapter 2
E2E テスト
1. E2E テストとは
E2E(End to End)テストは、UI(User Interface)テストとも呼ばれ、ユーザー操作の観点からアプリケーション全体の動作を確認するテストです。
読み方は「エンドツーエンドテスト」となります。
単体テストは、部分的な機能の確認はできますが、アプリケーション全体の中で適切に機能するかまでは確認することができません。
E2E テストは、ユーザーによる操作がアプリケーションにどのように影響するかをテストすることで、個別の機能の連携なども含め、より実際的な問題を検出することができます。
本チャプターでは、Cypress(サイプレス)というテストツールを使用して学習を進めていきます。
2. Cypress とは
Cypress は、Web アプリケーション用のフロントエンドテストツールです。
実際にブラウザを立ち上げて、画面に表示された文字を検索したり、ボタンをクリックしたり、画面遷移をしたりすることができます。
Cypress は、E2E テストのみではなく単体テストを行うことも可能ですが、本チャプターでは E2E テストの機能のみを使用します。
以下が公式ページです(URL: https://www.cypress.io/)。
なお、Vue 公式でも、E2E テストにおける推奨ツールとされています。
3. Cypress を追加した新規プロジェクトの作成
早速 Cypress を使用していきます。
手始めとして、create-vue ライブラリを使用して、Cypress がインストール済みの新しいプロジェクトを作成します。
以下の手順でプロジェクトの作成を行ってください。
① 作業フォルダに移動
まず、PowerShell を開いて、プロジェクトを作成するフォルダに移動します。
PS C:\Users\username> cd C:\vue_lesson
PS C:\vue_lesson>
② Vite プロジェクト生成コマンドの実行
Vue プロジェクトを作成するコマンドを入力します。
PS C:\vue_lesson> npm init vue@3
③ プロジェクト名の指定
プロジェクト名、ここでは「cypress-sample」と入力して Enter キーを押します(他の名前でも OK です)。
④ Cypress の選択
いつものように TypeScript の選択肢などが出てきますが、最初の 5 項目は「No」を選択していきます。
そして、次のように「Add an End-to-End Testing Solution?」という選択肢が出てきたときに「Cypress」を選択してください。
それ以降の選択肢は、全て「No」を選択します。
以下の状態になれば、プロジェクトの作成は完了です。
⑤ npm install の実行
プロジェクトが作成されたら cypress-sample
プロジェクトを VSCode で開き、ターミナルを開いて npm install
を実行します。
プロジェクトを作成する時期によって Warnig が出たりしますが、Error さえ出なければ OK としてください。
⑥ ローカルサーバを起動してブラウザで表示
npm run dev
コマンドを打ってサーバを起動しましょう。
以上のように表示されれば、新規プロジェクトの作成は完了です。
確認ができたら「ctrl + c」または「q」を入力して、サーバを停止しておいてください。
4. 自動生成されたコードを確認する
4-1. package.json ファイル
まず、package.json
ファイルから確認してみましょう。
① devDependencies に追加された内容
最初に、下の方の devDependencies
に記載されているライブラリから確認します。
Cypress を選択したことにより、以下の 2 つのライブラリが追加されています。
No | ライブラリ | 説明 |
---|---|---|
1 | Cypress | Vue 公式推奨の E2E テスト用ライブラリ |
2 | start-server-and-test | テスト実行時に自動的にサーバを立ち上げるライブラリ |
② scripts に追加された内容
次に、scripts
のところを見てみましょう。
Cypress を追加したことにより、以下の 4 行が追加されています。
"scripts": {
// 省略
"test:e2e": "start-server-and-test preview :4173 'cypress run --e2e'",
"test:e2e:dev": "start-server-and-test 'vite dev --port 4173' :4173 'cypress open --e2e'",
"test:unit": "cypress run --component",
"test:unit:dev": "cypress open --component"
}
E2E テストに使用するコマンドは、次の 2 つです。
No | コマンド | 説明 |
---|---|---|
1 |
test:e2e
|
E2E テストをブラウザを起動せず実行する。 |
2 |
test:e2e:dev
|
E2E テストをブラウザを起動して実行する。 |
4-2. 追加されたディレクトリ・ファイル
VSCode のエクスプローラーを見ると、以下のようなディレクトリとファイルが追加されています。
赤枠部分が、今回の E2E テストに関係する部分です。
青枠部分は、Cypress の単体テスト用のファイルのため、今回は使用しません。
上の図のファイルのうち、cypress\e2e\example.cy.js
と cypress.config.js
について、以下、見て行きます(赤矢印のファイルです)。
4-3. example.cy.js ファイル
まず、cypress\e2e\example.cy.js
ファイルを開きます。
コードは、次のようになっています。
Vitest と似たような構成になっています。
cypress\e2e\example.cy.js
describe('My First Test', () => {
it('visits the app root url', () => {
cy.visit('/')
cy.contains('h1', 'You did it!')
})
})
使用しているメソッドは、次のとおりです。
No | メソッド | 説明 |
---|---|---|
1 | describe | テストブロックを指定する |
2 | it | テスト内容を記述する |
3 | cy.visit |
指定した URL にアクセスする('/' の場合はベース URL)
|
4 | cy.contains | 指定したテキストを含む DOM 要素を取得する |
各メソッドについて、以下、簡単に見ていきます。
① describe メソッド
describe
メソッドは、テストブロックを指定するメソッドです。
describe('テストブロックの説明', () => {
// テストケースを記述
})
② it メソッド
it
メソッドは、テスト内容を記述するメソッドとなります。
it('テスト内容の説明', () => {
// テスト内容を記述
})
③ cy.visit メソッド
cy.visit
は、指定した URL にアクセスするメソッドです。
Cypress では、cy
に続けてメソッドを記述します。
cy.visit('URLを指定')
'/'
で始まる URL は、ベース URL からの相対パスとなります(ベース URL については後述します)。
④ cy.contains メソッド
cy.contains
は、文字列やセレクタから DOM 要素を取得するメソッドです。
cy.contains('検索文字列')
cy.contains('セレクタ', '検索文字列')
上記構文のほか、オプションの指定もすることができます(詳細は公式サイト参照)。
以上を踏まえて、example.cy.js
ファイルのテストコードを見てみましょう。
cy.visit('/')
cy.contains('h1', 'You did it!')
まず、1 行目の cy.visit('/')
で、ベース URL にアクセスしています。
次に、2 行目の cy.contains('h1', 'You did it!')
で、h1
タグの 'You did it!'
文字列を探して、その DOM 要素を取得しているということになります。
4-4. cypress.config.js ファイル
以下の cypress.config.js
ファイルは、Cypress の設定ファイルとなります。
上記画像の赤枠部分が、E2E テストに関する設定内容となっています。
cypress.config.js
e2e: {
specPattern: 'cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}',
baseUrl: 'http://localhost:4173'
}
各プロパティの意味は、次のとおりです。
No | メソッド | 説明 |
---|---|---|
1 | specPattern | テストファイルを格納するディレクトリを指定します。 |
2 | baseUrl | ベース URL を指定します。 |
ベース URL に 'http://localhost:4173'
が指定されていることが確認できます。
なお、specPattern の指定内容はデフォルト値のため、この指定がなくとも変わらず動作します。
4-5. 自動生成コードでテスト実行
自動生成されたテストコードを使用して、テストの実行をしてみましょう。
① E2E テストを起動する
以下のように npm run test:e2e:dev
とコマンドを打ち Enter キーを押してください。
次のように、ポート番号 4173 でローカルサーバが立ち上がります。
少し待つと Cypress のアプリケーションも立ち上がります(下図)。
初めて Cypress を使用する場合は、次のような画面が表示されますので「Continue」をクリックします。
次の画面でブラウザを選択することができます。
ここでは「Chrome」を選択して「Start E2E Testing in Chrome」ボタンをクリックします。
新たに、Chrome のブラウザが開きます。
このブラウザで E2E のテストを視覚的に行うことができます。
上記画像の赤枠部分に cypress\e2e
ディレクトリ内のファイルが表示されています。
先ほど確認した example.cy.js
ファイル(青枠部分)をクリックするとテストが実行されます。
② E2E テストの実行
テストファイル(example.cy.js
)をクリックすると、次のようにテストが実行されます。
緑色のチェックマークのところに、テストが 1 つ成功したことが表示されています
③ エラーを発生させる
ここで、エラーが発生した場合も確認してみましょう。
example.cy.js
ファイルを開いて、下の画像のように、検索文字列 You did it!
の後に 2
を追加します。
コードを書き換えて保存すると、テストが再実行されます。
以下のように赤字で「You did it!2
という文字列が見つからない」旨のエラーが表示されます。
エラー画面が確認できたら、検索文字列から 2
を削って、You did it!
に戻しましょう。
テストが再実行されて、以下のように緑色の成功の表示になれば OK です。
なお、青枠部分をクリックすると、手動でテストの再実行を行うことができます。
上の画像の赤枠部分のボタンを押すと元のトップ画面に戻ることができます。
④ E2E テストを終了する
一通りの確認ができましたので、E2E テストを終了しましょう。
Cypress アプリの「Close」ボタンを押すと、テストを実行していたブラウザを閉じることができます(なお、再表示した場合は「Focus」をクリックします)。
次に Cypress アプリを終了します。
以下のように「File」タブから「Close Window」を選択してください。
以上、Cypress の基本的な操作を確認しました。
5. Cypress で使用できるメソッド
Cypress で使用できるメソッドについて、主要なものを紹介します。
ほかにも様々なメソッドがありますので、詳しくは、公式ページをご覧ください。
5-1. アサーション
アサーションは、テストの評価を作成するメソッドです。
Vitest の expect
メソッドと同様の役割と考えて問題ありません。
No | メソッド | 説明/使用例 |
---|---|---|
1 | should |
アサーションを作成する。cy.get('nav').should('be.visible')
|
2 | and |
アサーションを作成する。処理内容は should と同じ(連結して使用)cy.get('nav').should('be.visible').and('have.class', 'open')
|
上の表中に出てくる cy.get('nav').should('be.visible')
は、nav
タブを取得して、それが可視状態か(be.visible
)を判定するものです。
また、.and('have.class', 'open')
は、open
という名前のクラスを持っているかを判定しています。
Cypress のアサーションでよく使用される構文を挙げると次の表のとおりです。
表中では .should
で記載していますが、.and
メソッドでも同様に判定できます。
アサーションの例 | 説明 |
---|---|
.should('be.visible')
|
見える(表示されている)。 |
.should('not.be.visible')
|
見えない(表示されていない)。 |
.should('have.class', 'hoge')
|
hoge というクラスを持っている。
|
.should('not.have.class', 'fuga')
|
fuga というクラスを持っていない。
|
.should('have.id', 'foo')
|
foo という id を持っている。
|
.should('not.have.id', 'bar')
|
bar という id を持っていない。
|
.should('be.checked')
|
チェックされている。 |
.should('not.be.checked')
|
チェックされていない。 |
.should('be.selected')
|
選択されている。 |
.should('not.be.selected')
|
選択されていない。 |
.should('be.empty')
|
要素が空である。 |
上の例はごく一部のものです。
主なゲッターとして、be
,have
を使用します。否定の場合は頭に not
を付けます。
アサーションの詳細については、公式ページを参照してみてください。
5-2. アクション
アクションは、テストプログラム内でボタンをクリックしたり、入力ボックスにテキストを入力したりすることができるメソッドです。
No | メソッド | 説明/使用例 |
---|---|---|
1 | click |
DOM 要素をクリックする。cy.get('button').click()
|
2 | dblclick |
DOM 要素をダブルクリックする。cy.get('button').dblclick()
|
3 | rightclick |
DOM 要素を右クリックする。cy.get('button').rightclick()
|
4 | trigger |
指定した DOM イベントを実行する。cy.get('a').trigger('mousedown')
|
5 | select |
セレクトボックスの選択を実行する。cy.get('select').select('user-1')
|
6 | check |
チェックボックスまたはラジオボタンをチェックする。cy.get('[type="checkbox"]').check()
|
7 | uncheck |
チェックボックスのチェックを外す。cy.get('[type="checkbox"]').uncheck()
|
8 | type |
DOM 要素に指定したテキストを入力する。cy.get('input').type('Hello, World')
|
9 | clear |
input または textarea の値をクリアする。cy.get('input').clear()
|
5-3. クエリ
クエリは、DOM から特定の要素や値を取得するために使用するメソッドです。
No | メソッド | 説明/使用例 |
---|---|---|
1 | children |
DOM 要素の子要素(複数)を取得する。cy.get('nav').children()
|
2 | contains |
指定したテキストを含む DOM 要素を取得する。cy.get('nav').contains('About')
|
3 | get |
セレクタを指定して DOM 要素を取得する。cy.get('li')
|
4 | find |
特定の DOM 要素内から DOM 要素を検索する。cy.get('.article').find('footer')
|
5 | eq |
DOM 要素の配列から指定したインデックスの要素を取得する。cy.get('tr').eq(0)
|
6 | first |
DOM 要素内の最初の DOM 要素を取得する。cy.get('a').first()
|
7 | last |
DOM 要素内の最後の DOM 要素を取得する。cy.get('a').last()
|
8 | title |
document の title を取得する。cy.title()
|
9 | url |
現在ページの URL を取得する。cy.url()
|
5-4. その他のメソッド
その他として、次のようなメソッドがあります。
No | メソッド | 説明/使用例 |
---|---|---|
1 | visit |
指定した URL にアクセスする。cy.visit('/')
|
2 | each |
配列等を反復処理する。cy.get('li').each(() => {...})
|
3 | submit |
form の submit を実行する。cy.get('form').submit()
|
4 | log |
Cypress アプリのログにメッセージを出力する。cy.log('created new user')
|
6. メソッドを使用したテストコードの記述
表だけでは分かりにくいので、上記の内の幾つかのメソッドを使用して、実際にコードを書いてみましょう。
6-1. HelloWorld.vue コンポーネントの修正
Cypress のメソッドを実行するため、src\components\HelloWorld.vue
ファイルの一部を修正します。
以下の画像の赤枠部分を追加してください。
コードをテキストで表示
src\components\HelloWorld.vue
<script setup>
import { ref } from 'vue'
defineProps({
msg: {
type: String,
required: true
}
})
const selected = ref('')
const toggle = ref(false)
const picked = ref(null)
const count = ref(0)
const increment = () => count.value++
</script>
<template>
<div>
<button @click="increment">Increment</button> {{ count }}
<div>
SELECT:
<select v-model="selected">
<option disabled value="" hidden>選択してください</option>
<option value="1">A</option>
<option value="2">B</option>
<option value="3">C</option>
</select>
{{ selected }}
</div>
<div>
CHECK: <input type="checkbox" v-model="toggle" /> {{ toggle }}
</div>
<div>
RADIO:
<input type="radio" value="One" v-model="picked">One
<input type="radio" value="Two" v-model="picked">Two
/ Picked: {{ picked }}
</div>
</div>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
You’ve successfully created a project with
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>
セレクトボックスやチェックボックスなど、テストで使用したいものを追加しています。
6-2. App.vue コンポーネントの修正
結果を見やすくするため、src\App.vue
ファイルの一部を修正します。
以下の画像の赤枠部分を削除(またはコメントアウト)してください。
コードをテキストで表示
src\App.vue
<script setup>
import HelloWorld from './components/HelloWorld.vue'
// import TheWelcome from './components/TheWelcome.vue'
</script>
<template>
<header>
<img alt="Vue logo" class="logo" src="./assets/logo.svg" width="125" height="125" />
<div class="wrapper">
<HelloWorld msg="You did it!" />
</div>
</header>
<main>
<!-- <TheWelcome /> -->
</main>
</template>
<style scoped>
header {
line-height: 1.5;
}
.logo {
display: block;
margin: 0 auto 2rem;
}
@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}
.logo {
margin: 0 2rem 0 0;
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}
}
</style>
以上のコード修正後、ブラウザでは次のように表示されます。
6-3. テストファイルの追加
それでは、テストコードを書くためのファイルを追加しましょう。
以下のように cypress\e2e
ディレクトリの直下に sample.cy.js
ファイルを追加してください。
記述したテストコードは以下のとおりです。
cypress\e2e\sample.cy.js
describe('テストサンプル', () => {
it('メソッドのテスト', () => {
cy.visit('/')
cy.log('クリック')
cy.get('button').click()
cy.log('トリガー')
cy.get('button').trigger('click')
cy.log('セレクトボックス')
cy.get('select').select('2').should('have.value', '2')
cy.get('select')
.select('C')
.should('have.value', '3')
.contains('C')
.should('be.selected')
.and('have.value', '3')
cy.log('チェックボックス')
cy.get('[type="checkbox"]').check().should('be.checked')
cy.get('[type="checkbox"]').uncheck().should('not.be.checked')
cy.log('ラジオボタン')
cy.get('[type="radio"]').first().check()
cy.get('[type="radio"]').eq(1).check()
})
})
以下、コードの内容を簡単に見ていきます。
6-4. テストコードの説明
① click/trigger/log
最初に、以下のところでボタンのクリックを行っています。
単純に click
メソッドを使用した場合と、trigger
メソッドでイベント名を指定した場合の 2 つを記述しています。どちらもボタンをクリックすることに変わりはありません。
cy.log('クリック')
cy.get('button').click()
cy.log('トリガー')
cy.get('button').trigger('click')
なお、log
メソッドで、実行したメソッドの内容をログに出力するようにしています。
② select/should/and
次に、セレクトボックスを選択する select
メソッドの使用部分です。
cy.get('select').select('2')
で、select タグ内の value が 2
の要素を選択しています。
cy.get('select').select('C')
の方は、表示テキストが C
の要素を選択するものです。
つまり、value でも表示テキストでも選択することが可能となっています。
cy.log('セレクトボックス')
cy.get('select').select('2').should('have.value', '2')
cy.get('select')
.select('C')
.should('have.value', '3')
.contains('C')
.should('be.selected')
.and('have.value', '3')
併せて should
メソッドおよび and
メソッドを使用してアサーション(テストの評価)を行っています。
例えば、.should('have.value', '3')
は、select タブの v-model
の値が
3
ということを判定しています。
なお、v-model
は、以下の HelloWorld.vue
の HTML の 1 行目の部分にあります。
<select v-model="selected">
<option disabled value="" hidden>選択してください</option>
<option value="1">A</option>
<option value="2">B</option>
<option value="3">C</option>
</select>
そして、テストコード中の .contains('C')
で、要素 <option value="3">C</option>
を取得し、それが選択中であること(.should('be.selected')
)を判定し、さらに、その value
の値が
3
であること(.and('have.value', '3')
)を判定しています。
③ check/uncheck/first/eq
続いて、セレクトボックス、ラジオボタンを操作する check
メソッドおよび uncheck
メソッドの部分です。
cy.log('チェックボックス')
cy.get('[type="checkbox"]').check().should('be.checked')
cy.get('[type="checkbox"]').uncheck().should('not.be.checked')
cy.log('ラジオボタン')
cy.get('[type="radio"]').first().check()
cy.get('[type="radio"]').eq(1).check()
cy.get(セレクタ).check()
でチェックを行い、cy.get(セレクタ).uncheck()
でチェックを外します。
ラジオボタンのところで出てくる first()
は複数ある要素のうち最初の要素を取得するものです。また、eq(1)
は、複数ある要素のうち、指定した index 番号の要素を取得するものとなります。
追加したテストコードの内容は、おおよそ以上のとおりとなります。
6-5. テストの実行
① Cypress アプリでテストを行う
それではテストを実行してみましょう。
ターミナルからコマンド npm run test:e2e:dev
を入力して Enter キーを押します。
Cypress のアプリが立ち上がったら「Chrome」を選択して「緑色の Start ボタン」をクリックします(下図)。
先ほど作成した sample.cy.js
ファイルが表示されていますので、それをクリックしてテストを実行します。
すると、以下のように、テストが実行されます。
赤枠部分が log
メソッドを使用して作成したログのテキストになります。
また、青枠部分は、アサーションの実行結果となります。
なお、下図のように、ログの途中の項目(赤枠部分)にマウスカーソルを重ねると、その状態のときの画面が表示され、関連する部分が色付きで表示されます(青枠部分)。
一通り確認を終えたら、Cypress アプリを終了します。
上の図の右上の「×」のところをクリックすることでも、アプリを終了させることができます。
② コマンドラインでテストを実行する
さて、テストを実行するコマンドには、もう一つ npm run test:e2e
というものがありましたので、今度は、それでテストを実行してみましょう。
ただし、このコマンドでテストを実行するには、一度アプリケーションをビルドする必要があります。
次のように、ターミナルで npm run build
コマンドを実行して、ビルドを行ってください。
ビルドが成功すると、左側のエクスプローラーのところに dist
というフォルダが作成されます(青枠部分)。
dist とは「distribution」の略で、配布用のファイルという意味になります。つまり、本番環境にアップするファイルはこのフォルダの中のファイルとなります。
npm run test:e2e
コマンドで実行するテストも、この dist
フォルダ内のファイルを使用して行われます(※)。
コードの修正と dist フォルダ
Vite のプロジェクトでコードを修正した場合、その修正内容は、自動的に dist
フォルダには反映されません。
そのため、コードを修正した後に npm run test:e2e
コマンドでテストを行う場合は、再ビルドを行い dist
フォルダ内のファイルも最新の状態に更新する必要があります。
なお、再ビルドのコマンドも npm run build
となります。
ビルドが完了したら、以下のように、ターミナルに、npm run test:e2e
コマンドを打ち込んでテストを実行してください。
テストが完了するまでには、少し時間が掛かりますので、気長に待ちましょう。
テストが完了すると、ターミナルにテスト結果が表示されます(下図)。
テストコードを記述した example.cy.js
ファイルと sample.cy.js
ファイルのテストが順に実行され、最後に、最終的な実行結果が表示されていることが確認できます。
なお、上の画像の緑枠の部分に Video output:
という表示があり、ファイルのパスが記述されています。
これらのファイルは、cypress\videos
フォルダの中に生成されています(下図赤枠部分)。
sample.cy.js.mp4
ファイルを実行すると、次のように、テストの実行内容を動画で確認することができます。
こちらのコマンドのテストも便利ですので、ぜひ、使い方を把握しておいていただければと思います。
7. 天気予報アプリに Cypress を導入する
このセクション以降では、Cypress の機能をより具体的に見ていくために、Lesson 9 で作成した「天気予報アプリ」を使用してテストを実行していきます。
7-1. Cypress のインストール
先ほどまでのテストは、Vite プロジェクト作成時に自動的に Cypress もインストールして使用していましたので、初期設定もせずに簡単に使用することができました。
しかし、実際の開発では、後からテストライブラリをインストールすることも多いです。
ここでも、手動で 1 つずつインストールを行って、初期設定をしていきましょう。
① インストール方法
Cypress のインストール方法は、以下の公式ページに説明されています。
書いてあるとおり、次のコマンドで Cypress のインストールをすることができます。
npm install cypress --save-dev
② 天気予報アプリに Cypress をインストールする
それでは、天気予報アプリのプロジェクト weather-app
を VSCode で開いて、ターミナルを表示し、次のように
npm install cypress --save-dev
コマンドを実行しましょう。
インストールが成功すると、package.json
に cypress
が追加されます。
③ start-server-and-test をインストールする
Cypress だけでもテストは実行できますが「start-server-and-test」ライブラリも一緒にインストールしておきます。
この start-server-and-test については「4. 自動生成されたコードを確認する」でも触れましたが、テスト実行時に自動的にサーバを立ち上げるライブラリとなります。テストが終了するとサーバの停止まで自動で行ってくれます。
インストール方法は、以下の npm の公式ページに記載されています。
つまり、次のコマンドでインストールをすることができます。
npm install --save-dev start-server-and-test
ターミナルに npm install --save-dev start-server-and-test
コマンドを打ってインストールを実行してください(下図)。
インストールが成功すると、次のように package.json
に start-server-and-test
が追加されます。
インストールは以上で完了です。
7-2. Cypress の初期設定を行う
必要なライブラリのインストールは行いましたが、Cypress の実行に必要なのフォルダやファイルはまだありません。
これらの初期設定については、Cypress が提供している機能を利用します。
① Cypress アプリの起動方法
次の公式ページに Cypress アプリの起動方法が紹介されています。
ここで指定されているコマンド npx cypress open
を使用することで Cypress アプリを起動することができます。
npx cypress open
② Cypress アプリの起動
weather-app
のターミナルに npx cypress open
コマンドを入力し Enter キーを押します。
数秒待つと、次のように Cypress アプリが立ち上がります。
最初に「E2E テスト」を行うか「コンポーネントテスト」を行うかの選択肢が現れますので、ここでは「E2E Testing」をクリックします。
③ 設定ファイル等の追加
E2E テストを選択すると、以下の画像ような表示が現れます。
タイトルは「Configuration files(設定ファイル)」となっており、その下に「 We added the following files to your project:(次のファイルをプロジェクトに追加しました)」と記載されています。
上の画像で表示された追加ファイルは、次の 4 つです。
ファイル | 説明 |
---|---|
cypress.config.js | Cypress の設定ファイル。 |
cypress\support\e2e.js | 各テストの実行前に呼び出されるファイル。共通設定などを記述。 |
cypress\support\commands.js | 共通処理(コマンド)を登録するファイル。 |
cypress\fixtures\example.json | テストで使用するテストデータ等を記述するファイル。 |
実際にプロジェクトに追加されたかを確認してみましょう。
VSCode を見ると、以下の赤枠部分に 4 つのファイルが追加されていることが確認できます。
以上の確認ができたら、Cypress アプリに戻って「Continue」をクリックしてください。
④ デフォルトのテストファイル追加
設定ファイルの確認画面の次は、ブラウザの選択画面となります。
これまでのように「Chrome」を選択して「緑色の Start ボタン」をクリックしてください。
現在は、まだテストファイルが 1 つも無いため、最初のテストファイルを作成する画面が表示されます。
ここでは「Create new spec」をクリックします。
次に、テストファイルの名前(パス)を指定する画面となります。
ここでは、デフォルトの名前のまま「Create spec」をクリックします。
今度は、テストコードの作成画面となります。
ここもデフォルトのままとして「Okay, run the spec」をクリックします。
すると、次のようにテストが実行されます。
このテストは Cypress が用意した URL https://example.cypress.io
で実行されますので、手元のアプリケーションとは関係なく成功するようになっています。
ここで作成した、デフォルトのテストファイルは、次のようにプロジェクトフォルダ内に追加されています。
この cypress\e2e
ディレクトリに、独自のテストファイルを追加していくことになります。
⑤ Cypress アプリの終了
ここまでの作業が終わったら、一旦、Cypress アプリを終了しましょう。
Cypress アプリの「Close」をクリックして、ブラウザを閉じます。
次に「File」タブから「Close Window」を選択して、アプリを終了させます。
以上で、Cypress に必要なフォルダとファイルが作成されました。
7-3. Cypress 設定ファイルの修正
ここで、Cypress の設定ファイルにベース URL を追加しておきましょう。
次のページを参考にします。
設定ファイル cypress.config.js
を開いて次の赤枠部分を追加します。
ベース URL の設定は、e2e
プロパティの中に記述します(e2e
プロパティに設定できるものは公式ページ参照)。
なお、記載しなくとも変わらないのですが video: true
という設定も追加しています。前セクションにおいて、コマンドからテストを実行したときに動画が生成されましたが、この動画の生成が不要な場合は video: false
とします(ここでは true
としておきます)。
7-4. デフォルトのテストコードを修正する
次に、デフォルトで作成されたテストファイル cypress\e2e\spec.cy.js
を修正して、天気予報アプリに対してテストの実行を行うようにしてみます。
① テストコードで実行する処理
まず、ここでは、以下の赤枠部分の文字 天気予報アプリ
と 全国の週間天気予報
が正しく表示されているかを確認するテストコードを書いてみます。
② App.vue コンポーネントの修正
天気予報アプリ
の文字は、以下の src\App.vue
ファイルの q-toolbar-title
タグ内に記述されています。この q-toolbar-title
タグは、Quasar のコンポーネント名のため、cy.get
メソッドでは取得することはできません。
ここでは、cy.get
メソッドで明示的に取得できるように(※)、q-toolbar-title
タグにクラス名 app-title
を追加しておきます(下図赤枠部分)。
コードをテキストで表示
src\App.vue
<script setup>
import { ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAreaStore } from './stores/area'
const route = useRoute()
const router = useRouter()
const areaStore = useAreaStore()
areaStore.fetchArea()
const selected = ref(null)
const options = ref([])
const filterFn = (val, update) => {
update(() => {
options.value = areaStore.officeArray.filter(v => v.name.indexOf(val) > -1)
})
}
const updateSelect = (code) => movePage(code)
const movePage = (code) => {
const name = route.name === 'week' ? 'week' : 'overview'
router.push({ name, params: { code } })
}
const leftDrawerOpen = ref(false)
const toggleLeftDrawer = () => leftDrawerOpen.value = !leftDrawerOpen.value
</script>
<template>
<q-layout view="hHh lpR fFf">
<q-header elevated class="bg-primary text-white" height-hint="98">
<q-toolbar>
<q-btn dense flat round icon="menu" @click="toggleLeftDrawer" />
<div class="row justify-between items-center full-width">
<q-toolbar-title class="app-title">
<q-avatar>
<img src="https://cdn.quasar.dev/logo-v2/svg/logo-mono-white.svg">
</q-avatar>
天気予報アプリ
</q-toolbar-title>
<div class="row items-center">
<span>地域検索: </span>
<q-select
outlined
v-model="selected"
dense
options-dense
use-input
hide-selected
input-debounce="0"
:options="options"
option-value="code"
option-label="name"
emit-value
map-options
@filter="filterFn"
style="width: 250px;"
bg-color="grey-2"
@update:model-value="updateSelect"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
検索結果なし
</q-item-section>
</q-item>
</template>
</q-select>
</div>
</div>
</q-toolbar>
<q-tabs align="left">
<q-route-tab to="/" label="ホーム" />
<q-route-tab :to="`/overview/${areaStore.code}`" label="天気概況" />
<q-route-tab :to="`/week/${areaStore.code}`" label="週間予報" />
</q-tabs>
</q-header>
<q-drawer show-if-above v-model="leftDrawerOpen" side="left" bordered>
<div
v-for="[code, name] in areaStore.offices"
:key="code"
@click="movePage(code)"
class="area-item"
>
{{ name }}
</div>
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<style lang="scss" scoped>
.area-item {
line-height: 28px;
padding: 2px 16px;
cursor: pointer;
&:hover {
background-color: #EEEEEE;
}
}
</style>
q-toolbar-title タグに付されるクラス
Quasar の q-toolbar-title
タグがレンダリングされると、実際は、次のような HTML となっています。
クラス名として q-toolbar__title
が付された div
タグとなるため、次のように記述して DOM 要素を取得することも可能ではあります。
cy.get('.q-toolbar__title')
③ Home.vue コンポーネントの確認
全国の週間天気予報
の文字は、src\views\Home.vue
ファイルの h4
タグ内に記述されています。
この DOM 要素については、cy.get('h4')
で、シンプルに取得できそうです。
④ spec.cy.js ファイルの修正
以上を踏まえて、テストファイル cypress\e2e\spec.cy.js
を次のように修正します。
テストコードの内容は次のとおりです。
cypress\e2e\spec.cy.js
describe('template spec', () => {
it('表示テキスト', () => {
cy.visit('/')
cy.get('.app-title').contains('天気予報アプリ')
cy.get('h4').contains('全国の週間天気予報')
})
})
まず、cy.visit('/')
で、アプリのベース URL からブラウザ表示を行います。
次に、cy.get('.app-title').contains('天気予報アプリ')
で、クラス名が「app-title」かつテキストに「天気予報アプリ」が含まれている DOM 要素を検索します。
最後に、cy.get('h4').contains('全国の週間天気予報')
で、タグが「h4」であり、テキストに「全国の週間天気予報」が含まれている DOM 要素の検索を行います。
contains
メソッドは、引数に指定したテキストを含む DOM 要素を取得するものですが、指定したテキストが見つからない場合はエラーを吐きますので、文字列の有無の検証にも使用することができます。
7-5. テストを実行する
テストコードが書けたら、テストの実行を行いましょう。
① E2E テストを指定してコマンドを実行する
Cypress アプリを立ち上げるコマンドは npx cypress open
でしたが、次のように、末尾に --e2e
を付加することで、テストの選択画面を飛ばして、ダイレクトに「E2E テスト」を実行することができます。
npx cypress open --e2e
一度、次のようにコマンドを打って実行してみましょう。
少し待つと、Cypress アプリが立ち上がります。
しかし、下の画像のように、ベース URL http://localhost:4173
に接続できませんとの警告が表示されてしまいます。
つまり、先にアプリのサーバを立ち上げて、アクセスできるようにしてあげる必要があるということです。
② ポートを指定してサーバを起動する
次の図の赤枠部分をクリックして、ターミナルをもう 1 つ表示させます。
いつものように、npm run dev
コマンドを実行すると、ポート番号 5173
でサーバが起動してしまいます。今回は、ポート番号 4173
を指定してサーバを起動する必要がありますので、次のコマンドを使用します。
npx vite --port <ポート番号>
なお、npm run dev -- --<ポート番号>
というコマンドでもポート番号を指定できます。
それでは、npx vite --port 4173
とコマンドを打ってサーバを立ち上げてみましょう。
上図のようにサーバが立ち上がったことが確認できたら、下の図の「Try again」をクリックします。
すると、いつものブラウザ選択画面が表示されます。
「Chrome」を選択して「緑色の Start ボタン」をクリックしてください。
③ テストを実行する
ブラウザが立ち上がったら「spec.cy.js」ファイルをクリックします。
少し時間が掛かると思いますが、以下のようにテストが実行されます。
④ テストを終了する
テストが無事に成功したら、Cypress アプリを終了させましょう。
下図の「×」ボタンを押せば、ブラウザが閉じ、Cypress アプリも終了します。
Cypress アプリを終了させると、下図の左側のテスト実行コマンドも終了します。
しかし、右側のローカルサーバは立ち上がったままです。
ローカルサーバについては「q」キーまたは「ctrl + c」で停止させます。
以上、天気予報アプリで簡単なテスト実行をすることができました。
7-6. テスト時にサーバを自動起動する
さて、先ほどのテストの方法では、ローカルサーバの立ち上げとテストの実行につきコマンドを 2 回実行する必要がありました。次のような手順となります。
(1) npx vite --port 4173
でアプリケーションのサーバを立ち上げる。
(2) サーバが立ち上がるのを待つ。
(3) npx cypress open --e2e
で Cypress のアプリを実行する。
ここでは、これを 1 回のコマンドで実行できるようにしていきます。
① start-server-and-test ライブラリの使用方法
Cypress のインストール時に「start-server-and-test」というライブラリもインストールしたことを覚えていると思います。
このライブラリを使用することで、上記の一連の操作を、まとめて行うことができます。
start-server-and-test を使用してコマンドを実行するには、次の構文で指定を行います。
npx start-server-and-test <サーバコマンド> <URL または ポート番号> <テストコマンド>
ここでは、次の指定を行います。
(1) サーバコマンド:npx vite --port 4173
(2) ポート番号::4173
(3) テストコマンド:npx cypress open --e2e
当てはめると、次のようなコマンドになります。
npx start-server-and-test 'vite --port 4173' :4173 'cypress open --e2e'
② start-server-and-test を使用してコマンドを実行する
それでは、VSCode のターミナルに次のコマンドを打ってE2E テストを実行してみましょう。
以下のように、Cypress のアプリが立ち上げれば成功です。
ここから Chrome を起動してテストを実行することができますが、特に新しいテストも作成していないので、そのまま右上の「×」ボタンを押してテストを終了します。
③ コマンドを package.json に登録する
さて、毎回次のようなコマンドを実行するのは大変です。
npx start-server-and-test 'vite --port 4173' :4173 'cypress open --e2e'
このコマンドを、package.json に登録して、いつでも実行できるようにしましょう。
次のように npx
を省いたコマンドを登録すれば、npm run test:e2e:dev
というコマンドで E2E テストを実行できるようになります。
"scripts": {
// 省略
"test:e2e:dev": "start-server-and-test 'vite --port 4173' :4173 'cypress open --e2e'"
}
実際に package.json
に登録すると、次のようになります。
説明は省きますが、併せて test:e2e
コマンドも登録しておきました。
コードをテキストで表示
package.json
{
"name": "weather-app",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test:e2e": "start-server-and-test preview :4173 'cypress run --e2e'",
"test:e2e:dev": "start-server-and-test 'vite --port 4173' :4173 'cypress open --e2e'"
},
"dependencies": {
"@quasar/extras": "^1.15.10",
"pinia": "^2.0.28",
"quasar": "^2.11.5",
"vue": "^3.2.45",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@quasar/vite-plugin": "^1.3.0",
"@vitejs/plugin-vue": "^4.0.0",
"cypress": "^12.5.1",
"sass": "^1.32.12",
"start-server-and-test": "^1.15.4",
"vite": "^4.0.0"
}
}
結果として、Vite プロジェクト作成時に自動生成されたコマンドと、ほぼ同じものになっています。
以上で、天気予報アプリへの Cypress の導入は完了となります。
8. 天気予報アプリで E2E テストを実行する
E2E テストを実行できる環境になりましたので、天気予報アプリに対していくつかのテストコードを書いて、正常に動作するかを確認していきます。
8-1. 左メニューページ遷移のテスト
まず、左メニューを操作した際に、正しくページ遷移をするか確認しましょう。
① テストコードの記述
cypress\e2e
ディレクトリに、以下のように、page-transition.cy.js
ファイルを追加してください。
テストコードの内容は次のとおりです。
cypress\e2e\page-transition.cy.js
describe('ページ遷移テスト', () => {
it('左メニューページ遷移', () => {
cy.visit('/')
// 左メニュー表示(左メニューが非表示であれば)
if (cy.get('.q-drawer').should('not.be.visible')) {
cy.get('button').click()
}
// 左メニューの青森県をクリック
cy.get('.area-item').contains('青森県').click()
// 青森県 天気概況が表示されているかを確認
cy.contains('h4', '青森県 天気概況').should('exist')
// ヘッダーメニューの週間予報タブをクリック
cy.get('header').find('a').contains('週間予報').click()
// 青森県 週間天気予報が表示されているかを確認
cy.contains('h4', '青森県 週間天気予報').should('exist')
})
})
② テストコードの説明
記述したテストコードにつき、簡単に説明をします。
・左メニューを表示
まず、cy.get('.q-drawer').should('not.be.visible')
で、左メニューが非表示か否かを確認して、非表示であればメニュー表示ボタンをクリックします。
.q-drawer
は、Quasar の q-drawer
コンポーネントに付けられるクラス名となります。
・左メニューから青森県の天気概況を表示
次に、cy.get('.area-item').contains('青森県')
で、左メニューから青森県を探し出してクリックをします。
その後、cy.contains('h4', '青森県 天気概況').should('exist')
で「青森県 天気概況」が表示されているかを判定(評価)しています。
・ヘッダータブから週間予報を表示
続いて、cy.get('header').find('a').contains('週間予報')
で、ヘッダーの「週間予報」タブを取得しクリックします。
最後に、cy.contains('h4', '青森県 週間天気予報').should('exist')
で「青森県 週間天気予報」が表示されているかを判定(評価)しています。
③ テストを実行する
それでは、テストを実行してみましょう。
ターミナルを開いて、npm run test:e2e:dev
コマンドを実行してください。
ブラウザの選択画面が表示されますので「Chrome」を選択してください。
次の画面が表示されたら、page-transition.cy.js
ファイルをクリックして、テストを実行します。
次のように、テストが実行されれば成功です。
なお、テストは正常に実行されたものの、各天気予報の API から情報を取得するときに、非同期通信が完了するまでの間の一瞬だけ、次のような「○○が取得できませんでした。」の表示がされていることが確認できます(これについては、後ほど補正を行います)。
テストが終わったら、Cypress アプリを閉じてテストを終了しておきましょう。
④ ターミナル内でテストを実行する
今度は、test:e2e
のコマンドで、Cypress アプリを起動せずに、テストを実行してみましょう。
ターミナルを開き、以下のコマンドでアプリケーションをビルドしてください。
npm run build
次に、以下の npm run test:e2e
コマンドでテストを実行します。
npm run test:e2e
ターミナルで、次のようにテスト結果が表示されれば成功です。
動画は、cypress\videos
ディレクトリに 2 つ保存されています。
page-transition.cy.js.mp4
には、次のような動画が作成されていると思います。
ここでも、折々に「○○が取得できませんでした。」の表示がされてしまっています。
これは、次の項で修正を行っていきます。
8-2. API 通信中のコンポーネント表示の修正
先に見たように「○○が取得できませんでした。」の表示がされてしまうのは、API からデータ取得を行う非同期通信を行っているときに、取得開始から終了までにタイムラグがあるためです。
レスポンスデータ取得完了までのわずかな時間の間、ストア内のレスポンスデータが「空」の状態であるため「○○が取得できませんでした。」の表示がされているということです。
この API との通信中の状態を、forecast ストアに持たせて、通信中は「○○が取得できませんでした。」の表示をしないように制御していきます。
① forecast ストアの修正
src\stores\forecast.js
ファイルを開いて、次の赤枠部分を追加してください。
コードをテキストで表示
src\stores\forecast.js
import { defineStore } from 'pinia'
import { useAreaStore } from './area'
export const useForecastStore = defineStore('forecast', {
state: () => {
return {
overview: {
headlineText: '',
publishingOffice: '',
reportDatetime: '',
targetArea: '',
text: '',
},
forecast: [],
forecastList: [],
loaded: false
}
},
actions: {
async fetchOverview(code) {
this.loaded = false
const areaStore = useAreaStore()
areaStore.code = code
const url = `https://www.jma.go.jp/bosai/forecast/data/overview_forecast/${code}.json`
try {
const response = await fetch(url);
this.overview = await response.json();
} catch {
this.$reset()
} finally {
this.loaded = true
}
},
async fetchForecast(code) {
this.loaded = false
const areaStore = useAreaStore()
areaStore.code = code
const url = `https://www.jma.go.jp/bosai/forecast/data/forecast/${code}.json`
try {
const response = await fetch(url)
this.forecast = await response.json()
} catch {
this.$reset()
} finally {
this.loaded = true
}
},
async fetchForecastList() {
this.loaded = false
const url = `https://www.jma.go.jp/bosai/forecast/data/forecast/010000.json`
try {
const response = await fetch(url)
this.forecastList = await response.json()
} catch {
this.$reset()
} finally {
this.loaded = true
}
}
}
})
ストアに loaded
プロパティ(ステート)を追加しています。
このプロパティが、API の通信中は false
となるように、各アクションに設定を追加しています。
以下、このプロパティを、各コンポーネントに適用していきます。
② Home コンポーネントの修正
src\views\Home.vue
ファイルを開き、以下のように v-if="forecastStore.loaded"
を追加します。
コードをテキストで表示
src\views\Home.vue
<script setup>
import { computed } from 'vue'
import { useForecastStore } from '../stores/forecast'
import WeekTable from '../components/WeekTable.vue'
const forecastStore = useForecastStore()
forecastStore.fetchForecastList()
const weatherList = computed(() => {
// 週間予報データが取得できていなければ return
if (forecastStore.forecastList.length === 0) return []
/** 天気データを格納するMapオブジェクト */
const list = new Map()
// 全国の地域ごとにループ処理
forecastStore.forecastList.forEach((forecast) => {
/** 1地域分のデータ */
const areaData = []
// 短期予報(Short-Range Forecast)のデータ処理
const srf = forecast.srf.timeSeries
// 2日間の降水確率を取得する
const pops = srf[1].areas.pops.concat() // 降水確率データをコピー
pops.unshift(...new Array(8 - pops.length).fill('-')) // 8 要素にする
// 2日間の温度(最低/最高)を取得する
const temps = srf[2].areas.temps.concat() // 温度データをコピー
temps.unshift(...new Array(4 - temps.length).fill('-')) // 4 要素にする
temps[0] = '-' // 1日目の最低温度は常に非表示
// 今日・明日の 2 日分のデータをループ処理
for (let j = 0; j < 2; j++) {
areaData.push(
{
timeDefines: srf[0].timeDefines[j], // 時間
areaName: forecast.name, // 地域名
weatherCode: srf[0].areas.weatherCodes[j], // 天気コード
pop: pops.slice(j * 4, j * 4 + 4).join('/'), // 降水確率
reliability: '-', // 信頼度(短期予報は値無し)
tempMin: temps[2 * j], // 最低温度
tempMax: temps[2 * j + 1] // 最高温度
}
)
}
// 週間予報のデータ処理
const week = forecast.week.timeSeries
for (let j = 1; j < week[0].timeDefines.length; j++) {
areaData.push(
{
timeDefines: week[0].timeDefines[j], // 時間
areaName: forecast.name, // 地域名
weatherCode: week[0].areas.weatherCodes[j], // 天気コード
pop: week[0].areas.pops[j], // 降水確率
// 信頼度(空文字の場合は '-' に変換)
reliability: !week[0].areas.reliabilities[j] ? '-' : week[0].areas.reliabilities[j],
tempMin: week[1].areas.tempsMin[j], // 最低温度
tempMax: week[1].areas.tempsMax[j] // 最高温度
}
)
}
list.set(forecast.officeCode, areaData)
})
return list
})
</script>
<template>
<div class="wrap">
<h4 class="heading">全国の週間天気予報</h4>
<template v-if="forecastStore.forecastList.length > 0">
<WeekTable :weatherList="weatherList" />
</template>
<template v-else>
<div v-if="forecastStore.loaded">全国の週間天気予報を取得できませんでした。</div>
</template>
</div>
</template>
<style lang="scss" scoped>
.wrap {
padding: 48px;
overflow: auto;
height: calc(100vh - 98px);
}
.heading {
margin-top: 0;
}
</style>
上の修正により、forecastStore.loaded
が false
の場合には「○○が取得できませんでした。」の表示をしないようにしています。
③ Overview コンポーネントの修正
続いて、src\views\Overview.vue
ファイルを開き、こちらも Home コンポーネントと同様に v-if="forecastStore.loaded"
を追加します。
コードをテキストで表示
src\views\Overview.vue
<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { useAreaStore } from '../stores/area'
import { useForecastStore } from '../stores/forecast'
import { onBeforeRouteUpdate } from 'vue-router'
const route = useRoute()
const areaStore = useAreaStore()
const forecastStore = useForecastStore()
/** 地域名 */
const officeName = computed(() => {
if (forecastStore.overview.targetArea !== '') {
return forecastStore.overview.targetArea
} else {
return areaStore.officeName
}
})
const weekDays = { 0: '日', 1: '月', 2: '火', 3: '水', 4: '木', 5: '金', 6: '土' };
/** 発表日時 */
const reportDateTimeDisplay = computed(() => {
if (!forecastStore.overview.reportDatetime) return '' // 日時が取得できない場合は return
const d = new Date(forecastStore.overview.reportDatetime) // 日時を Date 型に変換
const date = `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日` // 年月日
const weekDay = weekDays[d.getDay()] // 曜日
const time = `${d.getHours()}時${d.getMinutes()}分` // 時間
return `${date}(${weekDay}) ${time}` // 年月日・曜日・時間を連結して返却
})
forecastStore.fetchOverview(route.params.code)
onBeforeRouteUpdate(async (to) => {
forecastStore.fetchOverview(to.params.code)
})
</script>
<template>
<div class="wrap">
<h4 class="heading">{{ officeName }} 天気概況</h4>
<template v-if="forecastStore.overview.reportDatetime">
<table>
<tr><th>発表日時</th><td>{{ reportDateTimeDisplay }}</td></tr>
<tr><th>天気概況</th><td>{{ forecastStore.overview.text }}</td></tr>
</table>
</template>
<template v-else>
<p v-if="forecastStore.loaded">{{ officeName }} の天気概況を取得できませんでした。</p>
</template>
</div>
</template>
<style lang="scss" scoped>
.wrap {
padding: 48px;
overflow: auto;
height: calc(100vh - 98px);
}
.heading {
margin-top: 0;
}
tr {
vertical-align: top;
text-align: left;
white-space: pre-wrap;
th {
min-width: 80px;
padding: 8px;
}
td {
padding: 8px;
}
}
</style>
④ Week コンポーネントの修正
src\views\Week.vue
ファイルにも v-if="forecastStore.loaded"
を追加します。
コードをテキストで表示
src\views\Week.vue
<script setup>
import { computed } from 'vue'
import { useRoute, onBeforeRouteUpdate } from 'vue-router'
import { useAreaStore } from '../stores/area'
import { useForecastStore } from '../stores/forecast'
import { CODE_CONVERSION } from '../constant'
import WeekTable from '../components/WeekTable.vue'
const route = useRoute()
const areaStore = useAreaStore()
const forecastStore = useForecastStore()
forecastStore.fetchForecast(route.params.code)
onBeforeRouteUpdate(async (to) => {
forecastStore.fetchForecast(to.params.code)
})
const weatherList = computed(() => {
// 週間予報データが取得できていなければ return
if (forecastStore.forecast.length === 0) return []
/** 天気データを格納するMapオブジェクト */
const list = new Map()
/** 短期予報 1 つ目のエリアの地域コード */
let defaultCode = ''
// 短期予報(Short-Range Forecast)のデータ処理
const srf = forecastStore.forecast[0].timeSeries
// 地域(Area)数分ループする
for (let i = 0; i < srf[0].areas.length; i++) {
/** Mapオブジェクトのキーとなる地域コード */
const code = srf[0].areas[i].area.code
if (i === 0) defaultCode = code
// 2日間の降水確率を取得する
const pops = srf[1].areas[i].pops.concat() // 降水確率データをコピー
pops.unshift(...new Array(8 - pops.length).fill('-')) // 8 要素にする
// 2日間の温度(最低/最高)を取得する
const temps = srf[2].areas[i].temps.concat() // 温度データをコピー
temps.unshift(...new Array(4 - temps.length).fill('-')) // 4 要素にする
temps[0] = '-' // 1日目の最低温度は常に非表示
/** 1地域分のデータ(最初は短期予報の2日分を入れる) */
const areaData = []
// 今日・明日の 2 日分のデータをループ処理
for (let j = 0; j < 2; j++) {
areaData.push(
{
timeDefines: srf[0].timeDefines[j], // 時間
areaName: srf[0].areas[i].area.name, // 地域名
weatherCode: srf[0].areas[i].weatherCodes[j], // 天気コード
pop: pops.slice(j * 4, j * 4 + 4).join('/'), // 降水確率
reliability: '-', // 信頼度(短期予報は値無し)
tempMin: temps[2 * j], // 最低温度
tempMax: temps[2 * j + 1] // 最高温度
}
)
}
list.set(code, areaData)
}
// 週間予報のデータ処理
const week = forecastStore.forecast[1].timeSeries
for (let i = 0; i < week[0].areas.length && i < srf[0].areas.length; i++) {
/** Mapオブジェクトのキーとなる地域コード */
let code = week[0].areas[i].area.code
// 該当する地域コードがない場合
if (!list.has(code)) {
code = code in CODE_CONVERSION ? CODE_CONVERSION[code] : defaultCode
}
for (let j = 1; j < week[0].timeDefines.length; j++) {
list.get(code).push(
{
timeDefines: week[0].timeDefines[j], // 時間
areaName: '', // 地域名(短期予報の地域名を使用のため不要)
weatherCode: week[0].areas[i].weatherCodes[j], // 天気コード
pop: week[0].areas[i].pops[j], // 降水確率
// 信頼度(空文字の場合は '-' に変換)
reliability: !week[0].areas[i].reliabilities[j] ? '-' : week[0].areas[i].reliabilities[j],
tempMin: week[1].areas[i].tempsMin[j], // 最低温度
tempMax: week[1].areas[i].tempsMax[j] // 最高温度
}
)
}
}
return list
})
</script>
<template>
<div class="wrap">
<h4 class="heading">{{ areaStore.officeName }} 週間天気予報</h4>
<template v-if="forecastStore.forecast.length > 0">
<WeekTable :weatherList="weatherList" />
</template>
<template v-else>
<div v-if="forecastStore.loaded">{{ areaStore.officeName }} の週間天気予報を取得できませんでした。</div>
</template>
</div>
</template>
<style lang="scss" scoped>
.wrap {
padding: 48px;
overflow: auto;
height: calc(100vh - 98px);
}
.heading {
margin-top: 0;
}
</style>
⑤ テストを再実行する
修正が終わったら、再度テストを実行してみましょう。
ターミナルを開き、以下のコマンドでアプリケーションを再ビルドします。
npm run build
次に、npm run test:e2e
コマンドでテストを再実行します。
npm run test:e2e
テストが完了したら cypress\videos\page-transition.cy.js.mp4
ファイルが新しい動画に更新されています。
次のように「○○を取得できませんでした。」の表示がされなくなっていれば OK です。
今回の修正は「単に画面に表示させない」という簡素な方法を取りました。
実際の開発では「読み込み中」の文言や「スケルトンローダー」を表示する場合などが多いです。
興味がある方は、ご自身で調べて修正をしてみてください。
8-3. 検索ボックスからのページ遷移のテスト
次に、検索ボックスからページ遷移を行う場合についてテストを行っていきます。
① テストコードの記述
cypress\e2e\page-transition.cy.js
ファイルを開いて、次の赤枠部分を追加します。
1 つ目の it
の記述は折り畳んでいますので、行数の表示を見ながら追加してください。
コードをテキストで表示
cypress\e2e\page-transition.cy.js
describe('ページ遷移テスト', () => {
it('左メニューページ遷移', () => {
cy.visit('/')
// 左メニュー表示(左メニューが非表示であれば)
if (cy.get('.q-drawer').should('not.be.visible')) {
cy.get('button').click()
}
// 左メニューの青森県をクリック
cy.get('.area-item').contains('青森県').click()
// 青森県 天気概況が表示されているかを確認
cy.contains('h4', '青森県 天気概況').should('exist')
// ヘッダーメニューの週間予報タブをクリック
cy.get('header').find('a').contains('週間予報').click()
// 青森県 週間天気予報が表示されているかを確認
cy.contains('h4', '青森県 週間天気予報').should('exist')
})
it('検索ボックスページ遷移', () => {
cy.visit('/')
// 検索ボックスから岩手県を選択してページ遷移
cy.get('input').type('岩')
cy.get('.q-menu').contains('岩手県').click()
// 岩手県 天気概況が表示されているかを確認
cy.contains('h4', '岩手県 天気概況').should('exist')
// 左メニューから青森県を選択してページ遷移
if (cy.get('.q-drawer').should('not.be.visible')) {
cy.get('button').click()
}
cy.get('.area-item').contains('青森県').click()
// 青森県 天気概況が表示されているかを確認
cy.contains('h4', '青森県 天気概況').should('exist')
// 再度、検索ボックスから岩手県を選択してページ遷移
cy.get('input').type('岩')
cy.get('.q-menu').contains('岩手県').click()
// 岩手県 天気概況が表示されているかを確認
cy.contains('h4', '岩手県 天気概況').should('exist')
})
})
追加したテストコードの内容は次のとおりです。
cypress\e2e\page-transition.cy.js
it('検索ボックスページ遷移', () => {
cy.visit('/')
// 検索ボックスから岩手県を選択してページ遷移
cy.get('input').type('岩')
cy.get('.q-menu').contains('岩手県').click()
// 岩手県 天気概況が表示されているかを確認
cy.contains('h4', '岩手県 天気概況').should('exist')
// 左メニューから青森県を選択してページ遷移
if (cy.get('.q-drawer').should('not.be.visible')) {
cy.get('button').click()
}
cy.get('.area-item').contains('青森県').click()
// 青森県 天気概況が表示されているかを確認
cy.contains('h4', '青森県 天気概況').should('exist')
// 再度、検索ボックスから岩手県を選択してページ遷移
cy.get('input').type('岩')
cy.get('.q-menu').contains('岩手県').click()
// 岩手県 天気概況が表示されているかを確認
cy.contains('h4', '岩手県 天気概況').should('exist')
})
② テストコードの説明
記述したテストコードにつき、簡単に説明をします。
・検索ボックスから岩手県の天気概況を表示
まず、cy.get('input').type('岩')
で、検索ボックスに「岩」の文字を入力して地域名を検索し、
cy.get('.q-menu').contains('岩手県').click()
で「岩手県」をクリックします。
続いて、cy.contains('h4', '岩手県 天気概況').should('exist')
で「岩手県 天気概況」の表示がされているかを判定(評価)します。
・左メニューから青森県の天気概況を表示
次に、左メニューを表示の上、青森県をクリックしてページ遷移しています。
また、ページ遷移後に「青森県 天気概況」が正しく表示されているかを確認しています。
・再度、検索ボックスから岩手県の天気概況を表示
最後に、再度、検索ボックスから岩手県の天気概況を表示しています。
以上の操作により、「(1) 検索ボックスでページ遷移」→「(2) 左メニューでページ遷移」→「(3) 検索ボックスでページ遷移」という流れで、正しくページ遷移ができるかを検証しています。
③ テストを実行する
それでは、テストを実行します。
npm run test:e2e:dev
コマンドから Cypress アプリを開いて Chrome を選択します。
以下のように、ブラウザに表示されたファイル名から、page-transition.cy.js
ファイルをクリックして、テストを実行してください。
実行すると、テストコードの最後のところ(42行目)で、次のようなエラーが生じます。
エラーメッセージは「h4 セレクター内に '岩手県 天気概況' が見つかりません」というものです。
表示画面の方を確認すると、岩手県ではなく「青森県 天気概況」の画面が表示されていることが分かります。
これは、「(1) 検索ボックスでページ遷移」→「(2) 左メニューでページ遷移」→「(3) 検索ボックスでページ遷移」という流れの中で、(1) と (2) の処理は成功したが、(3) で失敗したという結果となっています。
テスト結果が確認できたら、Cypress アプリを閉じて、一旦、テストを終了しておきます。
8-4. テスト結果の検証
① page-transition.cy.js ファイルの確認
さて、先ほどのテストでは、page-transition.cy.js
ファイルの 42 行目でエラーが生じていました。
実際のコードを見ると、以下の 40 行目の「岩手県」のクリック(青枠部分)は成功したが、42 行目の文字列「岩手県 天気概況」の表示確認(赤枠部分)を失敗した、ということになります。
以上より「クリックはできたが、ページ遷移のイベントが正しく発火していなかった」などの原因が推測されます。
② App.vue ファイルの確認
クリック時のイベントは、App.vue コンポーネントで定義しましたので、そちらを確認してみます。
イベントをキャッチしているのは @update:model-value
のところです。
これは v-model
の値が変化したときのみイベントが発火するようになっています。
v-model
の値である selected
の状態は、テストケースに当てはめると次のようになっています。
No | 操作 | selected の値の変化 | 説明 |
---|---|---|---|
(1) | 検索ボックスで 岩手県をクリック |
null → '030000' |
検索結果選択時に、岩手県の地域コード 030000 に更新 |
(2) | 左メニューから 青森県をクリック |
'030000' で変化なし |
selected に変化はないためイベントは発火せず |
(3) | 検索ボックスで 岩手県をクリック |
'030000' で変化なし |
selected に変化はないためイベントは発火せず |
結論を言うと、最後の (3) で、検索ボックスから岩手県をクリックした際には、selected
の変化はなく、イベントが発火していないということになります。
以上を踏まえて、次の項で、天気予報アプリのコードを修正していきます。
8-5. 検索ボックスのイベント処理の修正
ここでは、一番簡単と思われる修正を行います。
App.vue
ファイルを開いて、次の赤枠部分のところを修正してください。
全てのコードをテキストで表示
src\App.vue
<script setup>
import { ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAreaStore } from './stores/area'
const route = useRoute()
const router = useRouter()
const areaStore = useAreaStore()
areaStore.fetchArea()
const selected = ref(null)
const options = ref([])
const filterFn = (val, update) => {
update(() => {
options.value = areaStore.officeArray.filter(v => v.name.indexOf(val) > -1)
})
}
const updateSelect = (code) => {
movePage(code)
selected.value = null
}
const movePage = (code) => {
const name = route.name === 'week' ? 'week' : 'overview'
router.push({ name, params: { code } })
}
const leftDrawerOpen = ref(false)
const toggleLeftDrawer = () => leftDrawerOpen.value = !leftDrawerOpen.value
</script>
<template>
<q-layout view="hHh lpR fFf">
<q-header elevated class="bg-primary text-white" height-hint="98">
<q-toolbar>
<q-btn dense flat round icon="menu" @click="toggleLeftDrawer" />
<div class="row justify-between items-center full-width">
<q-toolbar-title class="app-title">
<q-avatar>
<img src="https://cdn.quasar.dev/logo-v2/svg/logo-mono-white.svg">
</q-avatar>
天気予報アプリ
</q-toolbar-title>
<div class="row items-center">
<span>地域検索: </span>
<q-select
outlined
v-model="selected"
dense
options-dense
use-input
hide-selected
input-debounce="0"
:options="options"
option-value="code"
option-label="name"
emit-value
map-options
@filter="filterFn"
style="width: 250px;"
bg-color="grey-2"
@update:model-value="updateSelect"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
検索結果なし
</q-item-section>
</q-item>
</template>
</q-select>
</div>
</div>
</q-toolbar>
<q-tabs align="left">
<q-route-tab to="/" label="ホーム" />
<q-route-tab :to="`/overview/${areaStore.code}`" label="天気概況" />
<q-route-tab :to="`/week/${areaStore.code}`" label="週間予報" />
</q-tabs>
</q-header>
<q-drawer show-if-above v-model="leftDrawerOpen" side="left" bordered>
<div
v-for="[code, name] in areaStore.offices"
:key="code"
@click="movePage(code)"
class="area-item"
>
{{ name }}
</div>
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<style lang="scss" scoped>
.area-item {
line-height: 28px;
padding: 2px 16px;
cursor: pointer;
&:hover {
background-color: #EEEEEE;
}
}
</style>
検索ボックスからページ遷移するたびに、selected
の値を null
に初期化するという処理を追加しただけです。
これにより、検索ボックスから地域名を選択した際は、必ず selected
の値が変化することになります。
8-6. テストの再実行
コードの修正が終わりましたら、テストの再実行を行いましょう。
ターミナルを開き、npm run build
コマンドでアプリケーションを再ビルドし、ビルドが終わったら npm run test:e2e
コマンドでテストを実行します(下図)。
正しくコードの修正ができていれば、次のようにテストが成功するはずです。
cypress\videos\page-transition.cy.js.mp4
ファイルが新しい動画に更新されていますので、再生してみてください。
動画の内容が、次のようになっていれば OK です。
以上、Cypress の基本的な使い方について学習をしました。
今回のテストコードは、あらかじめ把握してあるエラー(バグ)が出るようにピンポイントで作成しましたが、実際のテストにおいては「テスト仕様書」などを用意して、様々なケースを漏れなく網羅することが必要となります。
テストの使用や設計の概念については、ここでは取り上げませんが、興味がある方はご自身で調べるなどしていただければと思います。
以上で、Vue の講座は完了となります。
おつかれさまでした。
