35
31

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

AngularAdvent Calendar 2019

Day 4

はじめてのユニットテストを書く前にモック実装で手が止まってしまわないために

Last updated at Posted at 2019-12-03

この記事は Angular Advent Calendar 2019 の 4 日目の記事です。

はじめに

ユニットテスト (UT) の重要性はずいぶん浸透してきました。
Angular を使って開発をする場合には、Angular CLI によって *.spec.ts の形でテストファイルが自動生成されているので、より身近なものとして感じられます。

ただ、重要性を理解はしていても、いざ UT を書き始めようと思い立ったところからテストコードを書くまでには意外と遠く感じられていないでしょうか。
そう思っている間に、ずっと自動生成されたままの *.spec.ts になってしまっていたり…

UT が遠のいてしまう理由

わたし自身は、テストコードを書くまでが遠く感じてしまう理由のひとつとして、モックの書き方を調べて理解するところで止まってしまうことでした。

コンポーネントでもサービスでも、それ単体で作成できてテストができることは、実際のサービスでは稀です。
現実的な開発では、他のコンポーネントやサービスとの関連があって成り立っています。

本質的なテストコードを書く事前準備の段階でつまづいてしまって、なかなか expect() を書くところまで至らずにいました。

そこで

そこで、本稿では、テストコードを書くにあったっての前準備となる代表的なモックの実装を列挙してみます。
まずはこれらを使うことで、UT の本質であるテストコードを書く部分へ意識を集中できるようになればと思います。

当然ながらモック自体もテストコードの一部分であり、きちんとテストについて学ぶことが大切です。
ただ、学びの初期の段階や途中では、「まずは動くものを書いてみる」ことも重要です。
本稿がその助力となれば幸いです。

こんな方へ

  • これから初めて UT を書こうとしている
  • UT の重要性は理解しているが、重い腰が上がらないでいる

あることないこと

書いてあること

  • UT の対象となるコンポーネントのコード
  • UT を書くためのモックコード

書いてないこと

  • Angular の文法および書き方
  • モックコードを書いたあとのテストコード自体

環境

下記を前提に記事を書いています。

  • Angular: 8.2.14
  • Angular CLI: 8.3.20

サービスの UT

Angular CLI で自動生成されたコードで事前準備はできています。
次のように、TestBed.get() することでサービスのインスタンスを取得することができます。

  beforeEach(() => TestBed.configureTestingModule({}));

  it('should call api', () => {
    const service: ApiService = TestBed.get(ApiService);
  });

コンポーネントの UT

コンポーネントを生成するときに必要なものとしては、テンプレートにディレクティブを記述するときに渡せる @Input の値と、依存性の注入 (DI: Dependency Injection) で注入されるサービスやオブジェクトが代表的です。

順番に見ていきます。

コンポーネント: @Input

次のような @Input が定義されているコンポーネントを、テスト対象として考えてみます。

mock-input.component.ts
export class MockInputComponent {
  @Input() title: string
}

次のコードは、Angular CLI によって自動生成された *.spec.ts を元にして、最後 3 行だけ追加したテストコードです。

mock-input.component.spec.ts
  let component: MockInputComponent
  let fixture: ComponentFixture<MockInputComponent>

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ MockInputComponent ]
    })
    .compileComponents()
  }))

  beforeEach(() => {
    fixture = TestBed.createComponent(MockInputComponent)
    component = fixture.componentInstance
    fixture.detectChanges()
  })

  it('should show title', () => {
    component.title = "test"  // プロパティとしてアクセスできる
  })

@Input は、コンポーネントのプロパティとしてアクセスできます。
プロパティに値を代入することもできますし、プロパティから値を取得することもできます。

コンポーネント: DI

次のようなコンストラクタでサービスを DI で注入しているコンポーネントを、テスト対象として考えてみます。

mock-service.component.ts
export class MockServiceComponent {
  constructor(private apiService: ApiService) { }
}

次のように、TestBed.configureTestingModule() を呼び出す前に、DI で注入するサービスのインスタンスを生成しておきます。
そして、そのインスタンスを providers の中で指定することで、コンポーネント生成時にサービスを注入することができます。

mock-service.component.spec.ts
  let component: MockServiceComponent
  let fixture: ComponentFixture<MockServiceComponent>

  beforeEach(async(() => {
    const apiService = new ApiService()  // サービスのインスタンスを作成する

    TestBed.configureTestingModule({
      declarations: [ MockServiceComponent ],
      providers: [
        { provide: ApiService, useValue: apiService }  // 作成したインスタンスを指定する
      ]
    })
    .compileComponents()
  }))

NgRx の UT

コンポーネントで DI をしている特別な例として、NgRx による状態管理をしていて Store を注入している場合があります。

mock-store.component.ts
export class MockStoreComponent {
  constructor(private store: Store<State>) { }
}

基本的には同様ですが、NgRx によって provideMockStore() が提供されています。

初期状態として initialState を準備して、それを元に MockStore が作成されるようにします。

  beforeEach(async(() => {
    const initialState: State = {}

    TestBed.configureTestingModule({
      declarations: [ MockStoreComponent ],
      providers: [
        provideMockStore({ initialState }),
      ]
    })
    .compileComponents()

    const store: MockStore<State> = TestBed.get(Store)
  }))

この MockStorereducer を持たないテスト用の Store になります。
MockStore の状態を変更したいときには、store.setState(nextState) を呼び出して新しい状態を与えて変更します。

まとめ

テストコードの前提となるモックコードを、テスト対象のコードと共に列挙しました。
これらを参考に UT を書くことを始めてみてはいかがでしょうか。

Angular のテスト全体については、公式のドキュメントがかなり充実していますので、そちらを参照してください。
Angular > コンポーネントクラスのテスト

また NgRx のテストについても、公式にドキュメントがありますので、そちらを参照してください。
@ngrx/store > Testing
@ngrx/effect > Testing

明日、Angular Advent Calendar 2019 の 5 日目は @Quramy さんです!

おまけ 1

下記のように、class の中で new でインスタンスを生成しているサービスがありました。
理想としては、適切なタイミングでリファクタリングをして、new している DocServiceClient DI で注入するように書きかえるべきかと思います。

export class DocService {
  client(): DocServiceClient {
    return new DocServiceClient()
  }

  public getDoc(): Promise<Doc> {
    return this.client().getDoc()
  }

「リファクタリングの前に UT を書く」ということで、次のようにテストコード中では DocServiceDocServiceClient を置き換えました。

  const setup = () => {
    ...
    const service = new DocService()

    // Use the spy object for this.client() like DI.
    const dsClientSpy = jasmine.createSpyObj<DocServiceClient>(
      'DocServiceClient',
      [
        'getDoc',
      ],
    )

    // Replace a return value of this.client() with the spy object.
    spyOn(service, 'client').and.returnValue(dsClientSpy)

    const dummyDoc: Doc = { docId: 1, name: 'Dummy' }

    return { service, dsClientSpy, dummyDoc }
  }

jasmine.createSpyObj<DocServiceClient>()DocServiceClient を置き換える Spy Object を作成しています。
作成した Spy Object を spyOn(service, 'client') を呼び出して、client() メソッドの返り値が Spy Object になるようにしています。

事前準備は以上で、あとは次のように、呼び出すメソッドの返り値を自由に変えることができるようになります。

  it('should get doc', (done: DoneFn) => {
    const { service, dsClientSpy, dummyDoc } = setup()

    dsClientSpy.getDoc.and.returnValue(Promise.resolve(dummyDoc))

    service.getDoc().then(res => {
      expect(res).toBeTruthy()
      done()
    })
  })

おまけ 2

テストを書いているときには、今記述しているテストのみ実行しながら試せると効率的です。
Jasmine では describe()it() の前に f 1 を付けた fdescribe()fit() が用意されています。

これらを使うと、f が付いたテストコードだけを実行することができるので便利です。

Jasmine > fdescribe
Jasmine > fit

  1. おそらく focused の f

35
31
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
35
31

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?