テストコードを書けるようになる
目標
- C#でテストコードの書き方を学び、業務に取り入れられそうなレベルまで学ぶ
- フロントエンドでも、個人開発などでテストコードが書けるレベルまで学ぶ
計画
1について
- xUnit.netのハンズオンコンテンツを進める
- 「テスト駆動開発」を読む
-
Blazorで何か作るときに取り入れる -
社内で共有し、業務に取り入れる
2について
- 「フロントエンド開発のためのテスト入門」を読む
FactとTheory
Fact
1つのテストメソッドに対して1つのテストのみ実行できる
例えば↓の例では1つのAddTest()
というメソッドに4
のみの値を入れている
Theory
複数のデータセットを定義することで1つのテストメソッドで複数のテストを実行できる
下の例ではIsOddWhenTrue()
というメソッドに対し3
と5
の値をセットできている
public static class Calculator
{
public static int Add(int x, int y) => x + y;
public static int Subtract(int x, int y) => x - y;
public static bool IsOdd(int value) => value % 2 == 1;
}
public class CalculatorTests
{
[Fact]
public void AddTest()
{
Assert.Equal(4, Calculator.Add(2, 2));
}
[Fact]
public void SubtractTest()
{
Assert.Equal(1, Calculator.Subtract(3, 2));
}
[Theory]
[InlineData(3)]
[InlineData(5)]
public void IsOddWhenTrue(int value)
{
Assert.True(Calculator.IsOdd(value));
}
}
SetupとTeardown
Setup:初期化処理
Teardown:終了処理
xUnit.netではテストクラスは、1つのテストメソッドを実行する都度、テストクラスのインスタンスが生成される。
その時に必要な、クラスのインスタンス生成や処理をテストクラスのコンストラクターに書き初期化処理(=Setup)を行う。
また、テスト実行後にDispose()
メソッドを実行することで終了処理(=Teardown)を行い、解放する。
public static class Files
{
public static bool DeleteIfExist(string file)
{
if (!File.Exists(file))
{
return false;
}
File.Delete(file);
return true;
}
}
public class FilesTests : IDisposable
{
private const string ExistFileName = "test.txt";
private const string TextFileContent = "Hello, xUnit.net!";
private const string NotExistFileName = "NotExistFile";
// setup
public FilesTests()
{
if (File.Exists(ExistFileName))
File.Delete(ExistFileName);
File.WriteAllText(ExistFileName, TextFileContent);
}
// teardown
public void Dispose()
{
if (File.Exists(ExistFileName))
File.Delete(ExistFileName);
}
[Fact]
public void DeleteIfExistWhenExistFile()
{
Assert.True(Files.DeleteIfExist(ExistFileName));
Assert.False(File.Exists(ExistFileName));
}
[Fact]
public void DeleteIfExistWhenNotExistFile()
{
Assert.False(Files.DeleteIfExist("NotExistFile"));
}
}
非同期処理
基本的に↑と同じようにして実行できる
public static async Task<string> ReadAllTextAsync(string file)
{
using (var reader = new StreamReader(File.OpenRead(file)))
{
return await reader.ReadToEndAsync();
}
}
// 初期化処理は省略
[Fact]
public async Task ReadAllTextAsyncWhenExistFile()
{
Assert.Equal(TextFileContent, await Files.ReadAllTextAsync(ExistFileName));
}
[Fact]
public async Task ReadAllTextAsyncWhenNotExistFile()
{
// 非同期用のAssertionを使用する
await Assert.ThrowsAsync<FileNotFoundException>(
() => Files.ReadAllTextAsync(NotExistFileName));
}
}
並行処理
xUnit.netでは単一のテストコレクションは逐次実行され、異なるテストコレクションは平行実行される
例えば以下の例である
// 単一のテストコレクションの例
// 逐次実行なので完了まで3+5=8秒かかる
public class UnitTest1
{
[Fact]
public void Test1()
{
Thread.Sleep(3000);
}
[Fact]
public void Test2()
{
Thread.Sleep(5000);
}
}
// 異なるテストコレクションの例
// 並行実行なので完了まで3<5で5秒かかる
public class UnitTest1
{
[Fact]
public void Test1()
{
Thread.Sleep(3000);
}
}
public class UnitTest2
{
[Fact]
public void Test2()
{
Thread.Sleep(5000);
}
}
逆に異なるテストクラス間で並列実行をしてほしくないときは以下のように同じコレクションとして登録すればOK
[Collection("Our Test Collection #1")]
public class UnitTest1
{
[Fact]
public void Test1()
{
Thread.Sleep(3000);
}
}
[Collection("Our Test Collection #1")]
public class UnitTest2
{
[Fact]
public void Test2()
{
Thread.Sleep(5000);
}
}
共有コンテキスト
ユニットテスト間でのロジックや初期化処理などの共有の行い方
コンストラクターとDispose
以下のように共通化したい初期化処理やDisposeを別クラスに定義する
public class HeavyFixture : IDisposable
{
// コンストラクター
public HeavyFixture() => Thread.Sleep(TimeSpan.FromSeconds(2));
public void Use()
{
}
// Dispose
public void Dispose() => Thread.Sleep(TimeSpan.FromSeconds(2));
}
public class UnitTest1 : IDisposable
{
// インスタンスの作成
private readonly HeavyFixture _heavyFixture;
public UnitTest1()
{
// 初期化(setup)
_heavyFixture = new HeavyFixture();
}
// 共通化したheavyFixuresクラスのUseメソッドを使用
[Fact]
public void Test1() => _heavyFixture.Use();
[Fact]
public void Test2() => _heavyFixture.Use();
// Dispose(teardown)
public void Dispose()
{
_heavyFixture.Dispose();
}
}
ロジックの共有はできるが、インスタンスの共有はできない(クラスごとに作成が必要)
前提として1つのテストごとにクラスのインスタンスが作られるので、上記では(初期化に2秒+Disposeに2秒)×2回=8秒
が必要となる
→別のテストクラスで同じ初期化とDisposeを行う必要があるのでそのたびに重い処理を実行しなければならない
→複雑なロジックの共有には適切だが、重たい処理の共有には本来向かない
クラス フィクスチャー
単一のテストクラス内でロジックとインスタンスを共有したい場合にクラスフィクスチャーを使う
// IClassFixture<T>を宣言する
public class UnitTest1 : IDisposable, IClassFixture<HeavyFixture>
{
private readonly HeavyFixture _heavyFixture;
// コンストラクター引数にフィクスチャーを追加する
// heavyFixureインスタンス作成=初期化
public UnitTest1(HeavyFixture heavyFixture)
{
_heavyFixture = heavyFixture;
}
// Test1とTest2でheavyFixureインスタンスを共有
[Fact]
public void Test1() => _heavyFixture.Use();
[Fact]
public void Test2() => _heavyFixture.Use();
// Disposeはフレームワーク側が実施してくれるためコメントアウト
public void Dispose()
{
//_heavyFixture.Dispose();
}
}
今回はインスタンスもの共有できているので(初期化に2秒+Disposeに2秒)×1回=4秒
で済む
コレクション フィクスチャー
異なるクラス間でコンテキストを共有したい場合にコレクションフィクスチャを使う
その前に↑でもでてきたコンストラクタでheavyFixureインスタンスを引数として受け取ってるもの何?readonlyで定義してる変数何?と思い調べた
↓
DI(Dependency injection):依存性の注入
書いてある通り、単体テストをしやすくしたり、コードを再利用しやすくするためのものらしい
本題
コレクションの定義
[CollectionDefinition("Heavy collection")]
public class HeavyCollection : ICollectionFixture<HeavyFixture>
{
// CollectionDefinitionを付与したクラスのみ作成すればよい
// 特別な実装は不要
}
// Collection属性を定義しコレクション定義の名称を設定
[Collection("Heavy collection")]
public class UnitTest1 : IDisposable
{
private readonly HeavyFixture _heavyFixture;
// コンストラクターで共有対象のフィクスチャーをインジェクション
public UnitTest1(HeavyFixture heavyFixture)
{
_heavyFixture = heavyFixture;
}
[Fact]
public void Test1() => _heavyFixture.Use();
[Fact]
public void Test2() => _heavyFixture.Use();
public void Dispose()
{
//_heavyFixture.Dispose();
}
}
// Collection属性を定義しコレクション定義の名称を設定
[Collection("Heavy collection")]
public class UnitTest2 : IDisposable
{
private readonly HeavyFixture _heavyFixture;
// コンストラクターで共有対象のフィクスチャーをインジェクション
public UnitTest2(HeavyFixture heavyFixture)
{
_heavyFixture = heavyFixture;
}
[Fact]
public void Test() => _heavyFixture.Use();
public void Dispose()
{
_heavyFixture.Dispose();
}
}
インスタンスを共有しているからクラスフィクスチャーと同様で4秒になる
ただし、コレクションフィクスチャーでは、同一コレクション内のすべてのテストメソッドは逐次実行されるためクラスフィクスチャーよりも時間はかかる
この仕様なのは、同一のリソースを利用することを想定しているから
とのこと
非同期ライフタイム
共有コンテキストには、非同期に初期化・終了処理を行うための、IAsyncLifetimeが用意されており、これまでと同様に使える
public class AsyncHeavyFixture : IAsyncLifetime
{
public Task InitializeAsync() => Task.Delay(TimeSpan.FromSeconds(2));
public void Use()
{
}
public Task DisposeAsync() => Task.Delay(TimeSpan.FromSeconds(2));
}
public class UnitTest3 : IClassFixture<AsyncHeavyFixture>
{
private readonly AsyncHeavyFixture _asyncHeavyFixture;
public UnitTest3(AsyncHeavyFixture asyncAsyncHeavyFixture)
{
_asyncHeavyFixture = asyncAsyncHeavyFixture;
}
[Fact]
public void Test() => _asyncHeavyFixture.Use();
}
これはクラスフィクスチャー
デバッグ出力
ITestOutputHelperを使うことでConsoleに出したいメッセージを出力することが出来る
private readonly ITestOutputHelper _output;
private readonly HeavyFixture _heavyFixture;
public UnitTest1(ITestOutputHelper output, HeavyFixture heavyFixture)
{
_heavyFixture = heavyFixture;
_output = output;
_output.WriteLine("This is output from {0}", "Constructor");
}
[Fact]
public void Test1()
{
_heavyFixture.Use();
_output.WriteLine("This is output from {0}", "Test1");
}
TheoryとDataAttribute
Theoryとは複数のデータセットを定義してテストを実行できるものであった
Inlinedata
[Theory]
[InlineData(3)]
[InlineData(5)]
public void IsOddWhenTrue(int value)
{
Assert.True(Calculator.IsOdd(value));
}
MemberData
InlineDataはシンプルに記述できるが、データセットの生成に何らかのロジックが必要な場合などは記述が困難になる、もしくは不可能となる
それを可能にするものにMemberdataとClassDataがある
MemberDataでは以下の3つのメンバーが利用可能
- メソッド
- プロパティ
- フィールド
いずれもIEnumerable<object[]>な値を返す、publicでstaticなメンバーである必要がある
都度データを生成する場合はメソッドを、一度生成したものを別のテストケースでも使いまわすのであればメソッド以外を利用するのが明確かもとのこと
1. メソッド
public static IEnumerable<object[]> GetValues() =>
new List<object[]>
{
new object[]{1, 2, 3},
new object[]{-1, -2, -3},
};
[Theory]
[MemberData(nameof(GetValues))]
public void MemberDataTestByMethod(int x, int y, int result)
{
Assert.Equal(result, Calculator.Add(x, y));
}
ここで出てくるnameof()
ってなんだ?!
↓
nameofは該当する変数や関数が存在しない場合はコンパイルエラーになる。どうやら保守性を高めるのに有効らしい。すぐ使えそう
2. プロパティ
public static IEnumerable<object[]> ValuesProperty { get; } =
new List<object[]>
{
new object[]{1, 2, 3},
new object[]{-1, -2, -3},
};
[Theory]
[MemberData(nameof(ValuesProperty))]
public void MemberDataTestByProperty(int x, int y, int result)
{
Assert.Equal(result, Calculator.Add(x, y));
}
3. フィールド
public static readonly IEnumerable<object[]> ValuesField =
new List<object[]>
{
new object[]{1, 2, 3},
new object[]{-1, -2, -3},
};
[Theory]
[MemberData(nameof(ValuesField))]
public void MemberDataTestByField(int x, int y, int result)
{
Assert.Equal(result, Calculator.Add(x, y));
}
ClassData
MemberDataで記載するとテストケースクラスにテストデータを生成するためのコードが大量に混在してしまう場合に有効
class AddTestDataSets : IEnumerable<object[]>
{
// メソッドを定義
public IEnumerator<object[]> GetEnumerator()
{
return new List<object[]>
{
new object[]{1, 2, 3},
new object[]{-1, -2, -3},
}.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
[Theory]
[ClassData(typeof(AddTestDataSets))]
public void ClassDataTest(int x, int y, int result)
{
Assert.Equal(result, Calculator.Add(x, y));
}
ClassData属性でデータセットのTypeを指定している。文字列ではない
読み始めた
「テスト駆動開発」を読む
等価性の検証
[Fact]
public void TestEquality()
{
Assert.True(new Dollar(5).CheckIsEquals(new Dollar(5)));
Assert.False(new Dollar(5).CheckIsEquals(new Dollar(6)));
Assert.True(new Franc(5).CheckIsEquals(new Franc(5)));
Assert.False(new Franc(5).CheckIsEquals(new Franc(6)));
Assert.False(new Franc(5).CheckIsEquals(new Dollar(5)));
}
public class Money
{
protected int amount;
public bool CheckIsEqual(Object obj)
{
Money money = (Money)obj;
return amount == money.amount && this.GetType() == money.GetType();
}
}
DollarとFrancはMoneyを継承している
インターフェースと抽象クラスについて
インターフェースには契約(contract)を定義するものを書く
抽象クラスには、継承するクラスによって抽象化したいメソッドなどがある場合に使う
機能はにているが、目的が違うのかな?
拡張メソッドについて
本読み終わった。
多分4割くらいしか理解できてない;;難しかった
- TDDは思っているものより開発を大きくDriveするものではなさそう
- あくまで実装を手助けする感じ
TDDのTは「テスト」の一部に過ぎない。
ソフトウェアの世界における本来のテストとは、認知外を探求する、いわば創造的破壊行為です。「TDDのテストとは、いわばプログラミングや設計の補助線、治具です。
つまり、テスト駆動開発のテストは、ソフトウェアテストの世界の「テスト」の中では一部を占めるだけに過ぎません。
それは"Checking"である
- 進め方は、テストファーストなのでまずテストから先に書き、レッドを出した後、実装を始め、グリーンが出るまでリファクタリングして、またテストして...のように繰り返していくこと
やってみた、読んでみた感じ、ただコンテンツ重視のWebサイトを作るだけであれば、わざわざテストを行う必要はなさそう。ソフトウェアみたいな機能がたくさんでより複雑なものであれば、凝集度を高くし、結合度を低くするための手助けとして導入するのはもちろん必要不可欠だと思うけど。
今1つもテストをやっていない環境からテスト手法を学び、実際のプロジェクトで導入するまでのコストや、導入できたとしてどれくらいい効果が出るのかを考えた時に、あまりメリットはないような感じがする。本にも書いてあったが、テストは実装を手助けするのはもちろんそうだが、コードが増えるという点では管理対象としての荷物が増えるということにもなるので、その均衡点みたいなものを超えていたら取り入れれば良いし、達していないならば取り入れなければいい。今はその均衡点に達していなさそうなので、これ以上必要以上に学ぶことはしない。(今後必要になりそうな時があればもちろんするが)
フロントエンドはどうなのだろうか。本が出版されたら読み進めたい。
やっと始めた
例外のスローを検証するテスト
expectの引数に値ではなく、関数を渡すため() => add(-10,10)
のようにアロー関数を渡す
describe("add", () => {("引数が'0〜100'の範囲外だった場合、例外をスローする", () => {
const message = "入力値は0〜100の間で入力してください";
expect(() => add(-10, 10)).toThrow(message);
expect(() => add(10, -10)).toThrow(message);
expect(() => add(-10, 110)).toThrow(message);
});
});
投げる例外でError型を継承した個別のエラークラスを定義する
// Eroorを継承したRangeErrorを定義
export class RangeError extends Error {}
function checkRange(value: number) {
if (value < 0 || value > 100) {
// 閾値を超えた場合はRangeErrorを投げる
throw new RangeError("入力値は0〜100の間で入力してください");
}
}
テストコード側はスローされるものが、作ったエラーインスタンスと等しいかで判定する
test("引数が'0〜100'の範囲外だった場合、例外をスローする", () => {
// 定義したRangeErrorインスタンスと等しいかで判定する
expect(() => sub(-10, 10)).toThrow(RangeError);
expect(() => sub(10, -10)).toThrow(RangeError);
expect(() => sub(-10, 110)).toThrow(Error);
});
配列の検証
describe("配列の検証", () => {
describe("プリミティブ配列", () => {
const tags = ["Jest", "Storybook", "Playwright", "React", "Next.js"];
test("toContain", () => {
expect(tags).toContain("Jest");
expect(tags).toHaveLength(5);
});
});
describe("オブジェクト配列", () => {
const article1 = { author: "taro", title: "Testing Next.js" };
const article2 = { author: "jiro", title: "Storybook play function" };
const article3 = { author: "hanako", title: "Visual Regression Testing " };
const articles = [article1, article2, article3];
test("toContainEqual", () => {
expect(articles).toContainEqual(article1);
});
// arrayContaining は引数の配列要素全てが検証先の配列に含めれていればOK
test("arrayContaining", () => {
expect(articles).toEqual(expect.arrayContaining([article1, article3]));
});
});
});
非同期処理のテストでtry-catchを使う場合
// try-cathcを使う場合
test("指定時間待つと、経過時間をもって reject される", async () => {
// アサーションの数を指定して正しい数のテストが行われているかを検証する
// このテストでは、1つのアサーションが行われることを期待している
expect.assertions(1);
try {
await timeout(50); // timeout関数のつもりが、wait関数にしてしまった
// ここで終了してしまい、テストは成功する
} catch (err) {
// アサーションは実行されない
expect(err).toBe(50);
}
});
- 非同期処理を含むテストは、テスト関数をasync関数で書く
- .resolversや.rejectsを含むアサーションはawaitする
- try-catch文による例外スローを検証する場合はexpect.assertionsを書く
レスポンスを再現するテストデータのことをフィクスチャーと呼び、fixtures.ts
のようなファイルに書く
アクセシブルネームの使用
formとその配下にあるinputなど、あるまとまり(group)に対してテストを実行したい時には、アクセシビリティの高いHTML要素を使用する
例えば、
type Props = {
onChange?: React.ChangeEventHandler<HTMLInputElement>;
};
export const Agreement = ({ onChange }: Props) => {
return (
<fieldset>
<legend>利用規約の同意</legend>
<label>
<input type="checkbox" onChange={onChange} />
当サービスの<a href="/terms">利用規約</a>を確認し、これに同意します
</label>
</fieldset>
);
};
この<fieldset>は暗黙のロールとして"group"ロールを持ち、テストの際にひとまとまりのグループとして特定できる。次にある<legend>は、グループのタイトルをつける時に使用する要素。
<div>だと、ひとまとまりのグループとして特定ができないためテストを行えなくなってしまう。そのため、テストを書くと同時にアクセシビリティも高めるという一石二鳥なことができる。
<input type="password">がロールを持たない理由
useId()を使ってformタグにアクセシブルネームを付与する。これがあることで、formロールが適用され、テストできるようになる。
useId()はReact 18で導入されたもので、aria-labelledby属性に唯一のIDを渡すのに使う。
import { useId, useState } from "react";
import { Agreement } from "./Agreement";
import { InputAccount } from "./InputAccount";
export const Form = () => {
const [checked, setChecked] = useState(false);
const headingId = useId();
return (
<form aria-labelledby={headingId}>
<h2 id={headingId}>新規アカウント登録</h2>
<InputAccount />
<Agreement
onChange={(event) => {
setChecked(event.currentTarget.checked);
}}
/>
<div>
<button disabled={!checked}>サインアップ</button>
</div>
</form>
);
};
export type AddressOption = React.ComponentProps<"option"> & { id: string };
export type Props = {
deliveryAddresses?: AddressOption[];
onSubmit?: (event: React.FormEvent<HTMLFormElement>) => void;
};
option要素が持つ属性たちとidを持つpropsの定義
deliveryAddressesに入れるもの↓
export const deliveryAddresses = [
{
id: "address_id_xxxx",
value: "address_id_xxxx",
children: "〒167-0051 東京都杉並区荻窪1-00-00",
},
];
onSubmitで使用するフォーム送信用のモック関数
function mockHandleSubmit() {
const mockFn = jest.fn();
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const data: { [k: string]: unknown } = {};
formData.forEach((value, key) => (data[key] = value));
mockFn(data);
};
console.log(mockFn);
console.log(onSubmit);
return [mockFn, onSubmit] as const; //タプルとして複数の戻り値をconst(=readonly)として定義
}
Formの一般化・抽象化
使う方
import { useState } from "react";
import { Form } from "../06/Form";
import { postMyAddress } from "./fetchers";
import { handleSubmit } from "./handleSubmit";
import { checkPhoneNumber, ValidationError } from "./validations";
export const RegisterAddress = () => {
const [postResult, setPostResult] = useState("");
return (
<div>
<Form
onSubmit={handleSubmit((values) => {
try {
checkPhoneNumber(values.phoneNumber);
postMyAddress(values)
.then(() => {
setPostResult("登録しました");
})
.catch(() => {
setPostResult("登録に失敗しました");
});
} catch (err) {
if (err instanceof ValidationError) {
setPostResult("不正な入力値が含まれています");
return;
}
setPostResult("不明なエラーが発生しました");
}
})}
/>
{postResult && <p>{postResult}</p>}
</div>
);
};
定義してる方
import { useState } from "react";
import { ContactNumber } from "./ContactNumber";
import { DeliveryAddress } from "./DeliveryAddress";
import { PastDeliveryAddress } from "./PastDeliveryAddress";
import { RegisterDeliveryAddress } from "./RegisterDeliveryAddress";
export type AddressOption = React.ComponentProps<"option"> & { id: string };
export type Props = {
deliveryAddresses?: AddressOption[];
onSubmit?: (event: React.FormEvent<HTMLFormElement>) => void;
};
export const Form = (props: Props) => {
const [registerNew, setRegisterNew] = useState<boolean | undefined>(
undefined
);
return (
<form onSubmit={props.onSubmit}>
<h2>お届け先情報の入力</h2>
<ContactNumber />
{props.deliveryAddresses?.length ? (
<>
<RegisterDeliveryAddress onChange={setRegisterNew} />
{registerNew ? (
<DeliveryAddress title="新しいお届け先" />
) : (
<PastDeliveryAddress
disabled={registerNew === undefined}
options={props.deliveryAddresses}
/>
)}
</>
) : (
<DeliveryAddress />
)}
<hr />
<div>
<button>注文内容の確認へ進む</button>
</div>
</form>
);
};
handleSubmit関数の定義
export function handleSubmit(callback: (values: any) => Promise<void> | void) {
return (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const values: { [k: string]: unknown } = {};
formData.forEach((value, key) => (values[key] = value));
return callback(values);
};
}
formのデータをKVの形に整形している関数
valuesには以下のようなデータが入る
{
phoneNumber: '000-0000-0000',
name: '田中 太郎',
postalCode: '167-0051',
prefectures: '東京都',
municipalities: '杉並区荻窪1',
streetNumber: '00-00'
}
AAA(Arrange-Act-Assert)パターン
Arrange: 準備、Act: 実行、Assert: 検証のこと
可読性が高くなるという特徴がある
test("バリデーションエラー時「不正な入力値が含まれています」が表示される", async () => {
render(<RegisterAddress />); // Arrange
await fillInvalidValuesAndSubmit(); // Act
expect(screen.getByText("不正な入力値が含まれています")).toBeInTheDocument(); // Assert
});
スナップショットテストでリグレッションを発生させる
正しい状態の(今回だとtaro)スナップショットがgit上にコミットされている状態で、以下のような間違ったスナップショットを作成する
test("Snapshot: アカウント名「taro」が表示される", () => {
const { container } = render(<Form name="jiro" />); // taroではなくjiroにしてみる
expect(container).toMatchSnapshot();
});
そうするとgit上でdiffが発生し、間違っていることがわかる
意図的な変更によるテストの失敗を成功させる
npx jest --updateSnapshot
または
npx jest -u
上記のコマンドを実行することで新しい状態のスナップショットに更新させる
カバレッジレポート
File:ファイル名称
Stmts:命令網羅率 全てのステートメントが少なくとも1回以上実行されたかを示す分数
Branch:分岐網羅率 全ての条件分岐が少なくとも1回以上実行されたかを示す分数
Funcs:関数網羅率 全ての関数が少なくとも1回以上呼び出されたかを示す分数
Lines:全ての行を少なくとも1回通過したかを示す分数
以下のコマンドでカバレッジレポートを生成
coverageフォルダーが自動生成される
npx jest --coverage
以下のコマンドを実行することでカバレッジレポートを視覚的に把握できる
open coverage/lcov-report/index.html
黄色と赤色はテストが不足していることを示す
↓こんな感じで網羅されていない部分は赤く塗りつぶされる
実装内部構造を把握し、論理的に書く「ホワイトボックステスト」にカバレッジレポートは欠かせません
カバレッジレポートの作成・開く
open __reports__/jest.html
アクセシビリティ観点を含めたコンポーネントの分類(AtomicDesign)
テストの話ではないが、JSXで以下のようにスプレッド構文でオブジェクトのkeyとvalueの対になる形でHTML要素の属性を書くと、HTMLに変換した際に以下のようにしてくれる
<a {...{"aria-current": "page"}}>
↓
<a aria-current="page">
プロパティの省略記法というものもある
const status = event.target.value;
push({ query: { ...query, status } });
これは、statusというプロパティにstatus="private"の値が入るようにできる
オブジェクトを生成するときにプロパティと同じ変数名の値であればプロパティの名前を省略して値だけを記述することができる
Next.jsのRouterに関するテストをJestで行いたいときはnext-router-mockを使う
pnpm add -D next-router-mock
使い方
test("「My Posts」がカレント状態になっている", () => {
mockRouter.setCurrentUrl("/my/posts"); // 現在URLが"/my/posts"であることを仮定する
render(<Nav onCloseMenu={() => {}} />);
const link = screen.getByRole("link", { name: "My Posts" });
expect(link).toHaveAttribute("aria-current", "page");
});
test.eachを使ったパターン
test.each([
{ url: "/my/posts", name: "My Posts" },
{ url: "/my/posts/123", name: "My Posts" },
{ url: "/my/posts/create", name: "Create Post" },
])("$url では $name がカレントになっている", ({ url, name }) => {
mockRouter.setCurrentUrl(url);
render(<Nav onCloseMenu={() => {}} />);
const link = screen.getByRole("link", { name });
expect(link).toHaveAttribute("aria-current", "page");
});
ただ、これらはNext.js 12以前のものなのでApp Routerで機能しない可能性が大きい
renderのテストをbeforeEachなどで書いてはいけない
Testing Libraryの作者Kent C. Doddsさんが書いた記事
setup関数を作ることで関数を抽象化して再利用できるようにする