トレタ開発者ブログ

飲食店向け予約/顧客台帳サービス「トレタ」、超直前予約アプリ「トレタnow」を開発・提供するスタートアップ企業です。

プロジェクトフィットなテスト計画をたてよう

本記事はトレタ Advent Calendar 2020 の 17 日目の記事です。

はじめまして。トレタでQAエンジニアをしています、福富(フクトミ)と申します。
3年ほど第三者検証を生業とする会社にてスクラムチーム内QAやQAチームリーダーを経験しまして、今年の7月にトレタに入社しました。
今回は「プロジェクトフィットなテスト計画をたてよう」というテーマでしゃべってみたいと思います。

テックブログに寄稿するのは人生で初めてなので、お手柔らかに見ていただけると嬉しいです。

はじめに

早速ですがQAエンジニアのみなさん、こんな経験はありませんか?

f:id:FukuromiQA:20201215215953p:plain
良かれと思ってやったのにどうしてこうなってしまったんだ。。。

「不具合がないこと」は品質を構成する1要素でしかありません。
いくら不具合がなくても、ユーザーから見て使いにくかったら。。。
システムへのアクセスにすごい時間がかかってしまったら。。。
「品質」を構成する要素はたくさんあるわけです。

。。。そう分かっていても、QAエンジニアとしてやっぱり不具合は1つも許したくないのです。
その考えのもと慎重にテストを行うと、上記のような状況に陥ることがあります。
というか自分は陥りました。

不具合なくリリースできること、市場に不具合が流出しないことは素晴らしいことです。
しかし、お客様のもとにシステムを届けるのが遅れてしまったり、
当初の予定よりコストが掛かってしまうことはサービスを提供する企業としてやはり避けなければなりません。
大事なのはプロジェクトに寄り添った(フィットした)テスト計画をたて、遂行するということなのです。

プロジェクトフィットなテスト計画をたてよう

さあ、ここからが本題です。
プロジェクトフィットなテストというのは、
 ・プロジェクトでやることに対してテストの抜け漏れがない
 ・かといって過剰にテストをしない
つまり、「ちょうどいい」テストってことですね!

では、そのプロジェクトフィットなテスト計画をたてるにはどうすればよいのでしょうか。
私が思う「これがわかっていればプロジェクトフィットなテスト計画をたてられるぞ!」という要素を紹介します。

プロジェクトの温度感を知ろう

よくプロジェクト管理の世界で利用される言葉に「QCD」というものがあります。

・Q:Quality(品質)
・C:Cost(コスト)
・D:Delivery(デリバリー、つまり納期)

プロジェクトの温度感をざっくりと知るには、上記QCDのどれを最優先とするかを聞いてしまうのが手っ取り早いでしょう。

f:id:FukuromiQA:20201216172510p:plain
QCDの優先度によってテストの粒度も変わってきます

スケジュールを知ろう

次に、プロジェクト全体のスケジュールを確認しましょう。
開発からリリースまで、どれくらいの期間がありますか?
テストの期間はどれくらい取ることができそうですか?
場合によっては、上で決めたQCDの優先度の変更を提案する必要もあるかもしれませんね!

f:id:FukuromiQA:20201216161619p:plain
どんなに目が痛くてもスケジュールはしっかり確認しましょう

「なぜやるのか」を確認しよう

なぜ、このプロジェクトを実施する必要があるのか?
このプロジェクトを実施することで、エンドユーザになにを届けられるのか?
QAだけでなくプロジェクト全体に関わる話ですが、必ずログとして残しておきましょう。
プロジェクトが迷走し始めたとき、全員で「なぜやるのか」を再確認することで立ち返ることができるかもしれません。

なにを作るのか確認しよう

当たり前の話ですが、プロジェクト成果物の要件もしっかり確認しておきましょう。
改修案件であれば、改修による影響範囲も把握できているといいですね!

まとめ

プロジェクトフィットなテスト計画を作るために把握しておきたいことを上で記述しました。
では、上記の情報はどうすれば知ることができるのでしょうか?

キックオフミーティングには必ず参加しよう

実は上記の情報すべてを1度に知ることができる機会があるんですね。
そう、プロジェクトキックオフミーティングです!
経験上、キックオフミーティングにQAメンバーが参加するというのはなかなかないのですが、ぜひ参加してください。
もし、これからキックオフミーティングを開こうとしている方がいらっしゃったら、QAメンバーも招集してあげてください。

品質はみんなで高めるもの

製品(サービス)を使って貰うユーザに「安心感」を持って頂くために開発者、提供組織が行うべき諸活動

これは、QA(Quality Assuranceつまり品質保証)という言葉をJaSST'17の講演(奈良 隆正先生)資料にていい感じに噛み砕いて説明してくれたものです。

「品質保証」というのは決してQAエンジニアがテストを行って不具合がないことを確認することだけではなく、
プロジェクトに関わるメンバー全員が参加して、ユーザに安心感を持っていただくために行われる活動である、ということですね!
より高品質なプロダクトを作るための活動をメンバー全員で実施していけたら、きっといいプロダクトになるんじゃないかなーと思います!
ということで、長くなりましたがこちらからは以上です!

終わりに

トレタは今色々な新しいことを始めているところです。
興味がある人はぜひ遊びに来てください。

仲間も募集しております!

それではこのへんで!またどこかでお会いしましょう!

自分なりに簡単なFlutterのアーキテクチャーを組み立ててみた

この記事は トレタ Advent Calendar 2020 の16日目です。

はじめに

トレタのフロントエンドエンジニアのClayです。元々はiOSエンジニアで現在はFlutter for WebとReactをメインでやっています。 今回は自分が使ってたFlutterアプリの簡単なアーキテクチャーややり方を紹介したいと思います。また最後はFlutter for Webを使った経験とメリデメを紹介したいと考えています。

FlutterのState管理

FlutterのState管理 と言えば昔から Bloc, Redux, か ScopedModel が人気だと思いますが、自分も前からずっとReduxを使ってFlutterのNativeアプリ開発をしてきました。今回は Flutter が公式で推進している Provider でやってみようかなと思いましたので自分なりなアーキテクチャーとルールを組み立てました。

アーキテクチャー

f:id:clay4649:20201215161811j:plain

基本は一方通行でViewから何かしたい時はController経由でServiceからデータ取ってきて、ControllerがそのデータでStateを更新してNotifierでViewがSelector Functionsで更新されるというアーキテクチャーです。データは基本Stateで管理しますがWidgetの状態は各StatefulWidgetで管理する方向です。例えばローディング状態とかStatefulWidgetで管理しちゃいます。StateはImmutableにしたいので freezed のPackageでImmutable化をしています。最初は自分的にbuild_runner みたいな3rd Partyコード生成ツールはあまりあまり好きじゃなかったので built_value とかは全然使っていませんでした。 freezed は割とbuild_valueよりは理解しやすいし、Equality OverloadcopyWithを全部実装してくれるので使い始まったらもうやめれなくなりました。

自分がReduxが好きな理由の一つは Single Source of Truth というコンセプトがとても好きです。例えばTodoアプリを作る場合はTodoのモデルは以下になります。

class Todo {
  const Todo({
    @required this.id,
    @required this.content,
    @required this.isDone,
    @required this.createdAt,
  });

  final String id;
  final String content;
  final bool isDone;
  final DateTime createdAt;
}

普通のStateだと以下のように todos を直接管理しますが、これだとWidgetはTodoを直接受け渡しないといけない(途中でTodoがどっかに変わっているかもしれない)ので、1つのTodoのReference(またはコピー)がいろんなところに存在する。また特定のTodoを取りたい場合は毎回検索しないといけないし、不便です。

class TodoState {
  const TodoState({@required this.todos});

  final List<Todo> todos;
}

そこで Redux の Normalized State のコンセプトをパクってきました。結果 State を freezed で Immutable 化して byIds のgetterを追加します。結果は以下みたいなStateができます。

@freezed
abstract class TodoState with _$TodoState {
  const factory TodoState({
    @Default(<Todo>[]) List<Todo> todos,
  }) = _TodoState;

  @late
  Map<String, Todo> get todoByIds => Map.fromEntries(todos.map((todo) => MapEntry(todo.orderId, todo)));
}

非常にいいところはこの freezed@late の魔法のマーカーです。公式ドキュメントによると

Freezed also contains early access to the upcoming late keyword. If you are unfamiliar with that keyword, what late does is it allows variables to be lazily initialized.

なので必要な時で計算されて、Stateが変わらなければもう計算はしないということなので非常的に効率的です。

これで todoByIds.key を取れば Todo のすべてのidが取れます。使い方としては Selector Function経由でデータを取って来れるので以下みたいなのでができます。

class TodoListView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Selector<SessionState, List<String>>(
      selector: (context, todoState) => todoState.orderItemById.keys,
      builder: (context, todoIds, _) => ListView.builder(
        itemBuilder: (context, index) => TodoTile(todoId: todoIds[index]),
      ),
    );
  }
}

または他のSortみたいなデータのロジックもここに入れられるので、特にSortとかはlazyやった方がいいので以下みたいに入れられます。

@late
Map<String, Todo> get todoIdsByCreatedAt => //Sort Function Here;

本当だったら todostodoByIds をprivateにして以下みたいに todoIdsgetTodo() でやるのは理想だと思いますが、開発チームはそんなに大きくないのでそこまでやる理由はあまりないかなと思いました。シンプルにして直接Objectを使わないようなルールだけにしました。

@freezed
abstract class TodoState with _$TodoState {
  const factory TodoState({
    @Default(<Todo>[]) List<Todo> todos,
  }) = _TodoState;

  @late
  Map<String, Todo> get _todoByIds => Map.fromEntries(todos.map((todo) => MapEntry(todo.orderId, todo)));

  @late
  Set<String> get todoIds => _todoByIds.keys.toSet();

  @late
  Todo get getTodo({@required String id}) => _todoByIds[id]
}

idで受け渡しするのは上手くやればWidgetのBuildの最適化にも繋がります。例えば、以下のようなObjectで受け渡しをしているはよく見かけますが

class TodoListTile extends StatelessWidget {
  const TodoListTile({
    @required this.todo,
  });

  final Todo todo;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(child: Text(todo.content)),
        Checkbox(
          value: todo.isDone,
          onChanged: (newValue) {
            // ....
          },
        )
      ],
    );
  }
}

この場合だと別れたtodoが変わると毎回この TodoListTile の全部のWidgetがRebuildされないといけないことになります。なのでできるだけ細かくSelectorのComponentに分けます。

class TodoListTile extends StatelessWidget {
  const TodoListTile({
    @required this.todoId,
  });

  final String todoId;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: Selector<TodoState, String>(
            selector: (context, state) => state.todoById[todoId].content,
            builder: (context, content, _) => Text(content),
          ),
        ),
        Selector<TodoState, bool>(
          selector: (context, state) => state.todoById[todoId].isDone,
          builder: (context, isDone, _) => Checkbox(
            value: isDone,
            onChanged: (newValue) {
              // ....
            },
          ),
        ),
      ],
    );
  }
}

SelectorはSelector Functionの結果が変わる時のみRebuildされるので、こんな感じにしたら isDone が変わる時だけ(Todoのチェックボックスが押される時だけ)は Checkbox のみがRebuildされます。この例だとChildrenのWidgetは2つしかないですが、もっと大きいWidgetだとChildrenも多くなって更にそのChildrenのChildrenも全部 Rebuild されたらあまり効率的ではないのでこのidで受け渡してSelectorでデータ取ってくる方法はかなり効率だと思います。

またよく使うSelectorのComponentを切り分けでしたらまたいろいろ共通化できます。テキストとかはこんな感じで便利です。

import 'package:provider/provider.dart';
import 'package:flutter/material.dart';

/// Storeから直接テキストを作成する
/// Example
/// ```
/// SelectorText<AppState>(
///   selector: (context, appState) => appState.name,
///   style: TextStyle(
///     color: Colors.white.withAlpha(60),
///     fontSize: 14,
///   ),
/// ),
/// ```
class SelectorText<S> extends StatelessWidget {
  const SelectorText({
    @required this.selector,
    @required this.style,
    this.overflow,
    this.maxLine,
  });

  /// このテキストを使うセレクタ
  final String Function(BuildContext, S) selector;

  /// `Text`みたいな`TextStyle`
  final TextStyle style;

  /// `TextOverflow`の指定
  final TextOverflow overflow;

  final int maxLine;

  @override
  Widget build(BuildContext context) {
    return Selector<S, String>(
      selector: selector,
      builder: (context, text, _) => Text(
        text ?? '',
        style: style,
        overflow: overflow,
        maxLines: maxLine,
      ),
    );
  }
}

やってよかったこと

  • 表示ロジックはSelector Functionに入っているので Unit Testがやりやすい
  • Objectを深く階層で受け渡しはなくなる
  • けっこうFlexibleな構成でいろんなエンハンスを対応できる
  • ChildのWidgetがBuildされる数が減る
  • Single Source of Truthが守られる
  • 構成がシンプルで実装しやすい
    • WidgetのStateはもうStateless Widgetに任せる

おまけ:Flutter for Webやってみた印象

メリット

  • Web アプリをアプリぽくするのは簡単
    • Webとアプリはほぼ90%+同じコードでできる
  • UI 実装が非常的に早い
    • 用意されるMaterial Widgetを使うかそのWidgetをいろいろカストマイズするのもらくらくにできた
    • ゼロから自分で組み立てるのも可能なのでだいたいはなんでも作れるような気がする
  • CSSってなんですか?
  • --releaseの時は意外とパフォーマンスがそんなに悪くない
  • アプリとほぼ同じやり方と考え方でWebアプリを作れる 

デメリット

  • まだBetaなのでBugが多い
    • iOSのSafariでスクロールする時は普段NavigationとUrlバーが隠られるはずだが、FlutterだとCanvasの中の疑似スクロールのためUrlバーが隠れていなくてスマートフォンの画面サイズが小さく見える(iOSのSafariのみ)
      • Full Screen Apiでなんとかしようと思ったら iPhoneのSafariだけがFull Screen Api を対応していない
  • FlutterのライブラリーがWebをサポートしていない場合はけっこうある
  • Hot ReloadじゃなくてHot Restartしかできないからコード保存してHot RestartされたらNativeアプリと違ってStateが全部が消える
    • 階層が深い画面で開発したらHot Restartされたら最初の画面から始めないといけないから結局開発中だけその画面をrootに切り出さないといけない
  • SVGが簡単に使えない
  • 新しい仕組みのSkia (いろいろをツルツルしてくれる)が日本語(Special Characters)をサポートされていない
  • FLUTTER_WEB_USE_EXPERIMENTAL_CANVAS_TEXT=false 付けないと日本語がよく切られるが、このフラグは --release 時のみしか機能しないため debug 中は日本語が切られてままでやっている
  • SSRがかんたんにできない
  • ルーティングがカオス(Nativeアプリ向けのまま)

Flutter for Webのまとめ

デメリットが多いように見えますが、自分的にはWeb開発による面倒くささを関わらなくてもWebアプリがラクラクでできるFrameworkだと思っていますのでみんなもFlutterを試してもらえたら嬉しいです。

終わりに

トレタは今色々新しいことができるところです。Flutterもその一つです。 興味ある方はぜひ遊びに来てください。

仲間も募集しております!

Gauge+Capybara+Rspecではじめる自動テスト

この記事はトレタ Advent Calendar 2020の15日目の記事です。

こんにちは。トレタでQAエンジニアやってます、中村です。 今年はGaugeを使ってE2Eテスト実装を行ったのですが、復習も兼ねて簡単に使い方をまとめてみようと思います。 この記事で説明しているコードはこちらに挙げてますので、一緒に見てみてください。

github.com

Gaugeとは

GaugeはThoughtWorks社がOSSで開発しているテスト自動化フレームワークです。 gauge.org 主な特徴として、以下が挙げられます。

  • 受け入れテスト自動化レイヤーのツール
  • テストの記述をmarkdownで可能
  • テストと実装を分けることができる
  • 自動テストに必要な機能が充実
    • TestRunner,Data-driven testing,Report,Screenshot,IDE
  • 多くの言語サポート
    • Java,Python,C#,Ruby,JavaScript

Seleniumでテスト自動化を行おうとするそれ以外の実装を行う必要が出てくるので、テストに必要な周辺機能がオールインワンで揃ってるのはうれしいところですね。

Gaugeのインストール

公式ドキュメントも詳細なので迷わずセットアップが可能です。 docs.gauge.org macの場合はbrew install gaugeでインストール可能です。

言語プラグインのインストールとプロジェクト作成

Gaugeをインストールしたら、次は言語プラグインをインストールします。 今回はRubyを指定しました。

gauge install ruby

インストール後は下記のコマンドでプロジェクト作成を行います。

gauge init ruby

プロジェクトディレクトリを作成したら、gauge run specs してテストを実行してみましょう。下記のようにテストがPassすればOKです。

f:id:gonkm:20201215033429p:plain

CapybaraとRspecの導入

今回はGaugeを使ってブラウザテストを行うため、CapybaraとRspecを導入します。 Gemfileに下記を追記して、bundle installします。

gem 'rspec'
gem 'selenium-webdriver'
gem 'capybara'

インストールが完了したらstep_inplementaions内に、spec_helper.rbを準備します。 Execution Hooksbefore_suiteにテストスイート実行前にCapybaraを起動するための記述を追加しています。 またGauge.configure経由でCapybara::DSLRSpec::Matchersをincludeします。 Gauge.configureにはScreenshot取得のための記述も追加しておきます。

require 'capybara/dsl'
require 'capybara/rspec'
Bundler.require

Capybara.default_driver = :chrome

before_suite do
  Capybara.register_driver(:chrome) do |app|
    options = Selenium::WebDriver::Chrome::Options.new
    options.add_argument('--headless')
    options.add_argument('--window-size=1400,1400')

    Capybara::Selenium::Driver.new(app, browser: :chrome, options: options)
  end
end

Gauge.configure do |config|
  config.include Capybara::DSL
  config.include RSpec::Matchers
  config.custom_screenshot_writer = -> {
    file = File.join(config.screenshot_dir, "screenshot-#{(Time.now.to_f*10000).to_i}.png")
    Capybara.page.save_screenshot(file)
    File.basename(file)
  }
end

テストの実装とテストケースの作成

テストケースの作成を行うには、markdown内で利用するstepの実装が必要です。 step_inplementaion.rb内に、下記の内容を記述します。

step 'Googleで「トレタ」と検索する' do
  Gauge.write_message 'Googleへ遷移'
  visit('https://www.google.com/?hl=ja')
  find('input[name="q"]').set('トレタ')
  find('input[name="btnK"]').click()
  Gauge.capture
  expect(page).to have_content('株式会社トレタ(Toreta,Inc.)')
end

Capybaraのコードに加えて、テストの結果出力のために下記を記述しています。

  • Gauge.write_message:指定した文字列を出力
  • Gauge.capture:bowスクリーンショットの取得

テストケースはspecs内にXXX.specという拡張子で作成します。 以下のように、sample.specを作成し、実装したstepを記述します。

# Googleでトレタを検索する

## Googleでトレタを検索する

* Googleで「トレタ」と検索する

テストの実行とレポートの確認

テストの実行はspecファイルを指定して実行します。

gauge run specs/sample.spec

f:id:gonkm:20201215043118p:plain

出力されるHTMLレポートは下記の通りです。 stepで実装した文字列とスクリーンショットも反映されています。

f:id:gonkm:20201215043626p:plain

データ駆動型テストの実装

前述の通り、Gaugeではデータ駆動テストをサポートしています。 markdownにデータテーブルを準備し、stepでパラメータを受け取るようにすることで、実装が可能です。 先程実装したテストをデータ駆動型に書き換えてみます。

sample2.spec

# Googleでトレタのサービスを検索する

| keyword                   | result                                                  |
|-------------------------- |---------------------------------------------------------|
| トレタ予約台帳              | あらゆる飲食店・レストランの繁盛を支える予約台帳/顧客台帳サービス  |
| トレタかんたんウェブ予約      | 無料でウェブ予約ページの作成が可能                            |
| トレタnow                  | グルメの、超直前予約アプリ                                   |

## Googleでトレタのサービスを検索し、検索結果を確認する

* Googleで「<keyword>」と検索する
* 検索結果に<result>が含まれていることを確認する

step_inplementaion.rb

step 'Googleで「<keyword>」と検索する' do |keyword|
  Gauge.write_message 'Googleへ遷移'
  visit('https://www.google.com/?hl=ja')
  find('input[name="q"]').set(keyword)
  find('input[name="btnK"]').click()
end


step '検索結果に<result>が含まれていることを確認する' do |result|
  Gauge.capture
  expect(page).to have_content(result)
end

結果

どのデータテーブルのテストかがわかりやすく表示されます。

f:id:gonkm:20201215051343p:plain f:id:gonkm:20201215051357p:plain

実際に導入してみて

今回の記事ではブラウザテストベースでの使い方をまとめましたが、弊社での実際の使い方としてはトレタ予約台帳と外部連携しているAPIに対して導入しています。 外部連携ではテストの事前準備として連携各社様の予約画面からブラウザを操作して予約を作成する必要があったためです。

APIテスト単体であればPostmanで事足りますが、手順が長く複雑なE2Eレベルのテストシナリオをユーザー操作レベルで記述するという面では、Gaugeのように自然言語でドキュメンテーションを残せるのは大きなメリットがあっため、今回採用してみました。 前提条件を作成するための手順が長い場合は、Conceptを作ることでspec上の記述を簡略化させたりできるため、冗長な記述を避けることができる点もよかったです。

テストの実装タイミングは保守開発が進んでいる状況だったのでエンハンスやバグ修正のテストを実装しつつ、リグレッションテストを増やしていきました。現在ではCircleCIで週1で定期実行を行っている状態です。

最後に

現在トレタは新しい取り組みを多く進めています!興味のある方はぜひとも遊びに来てください!

corp.toreta.in

© Toreta, Inc.

Powered by Hatena Blog