トレタ開発者ブログ

飲食店向け予約/顧客台帳サービス「トレタ」、モバイルオーダー「トレタO/X」などを運営するトレタの開発メンバーによるブログです。

2020年のトレタQAチーム - 今年変わったこと、これからのこと -

* 本記事は トレタ Advent Calendar 2020 の18日目の記事です

 こんにちは。引っ越しから3週間、ダンボールがいつまで経っても片付かないQAエンジニアの林です。
 今回は、トレタのQAチームに今年1年で起きた変化と、今後さらに変えていきたいことについて書いていこうと思います。

チーム紹介

 2020年1月時点でQAエンジニアは3名。7月に1名ジョインし、以降は4名体制に。
 チームリーダーは技術部全体を統括するマネージャーが兼任してくださっていました。
 現在トレタには複数のプロジェクトが存在しているため、QAメンバーは各プロジェクトチームに所属し、個々でタスクをこなしプロジェクトへ貢献するのが主となっています。

今年1年で変わったこと

QA定例のオンライン化

 トレタでは、コロナウイルスへの対応として3月末から原則リモートワーク、6月からはリモートワーク/出社が選択できるようになりました。 note.com

 それに伴い、週次で行っているQA定例もオンライン開催をする運びとなりました。
 内容はオフライン開催をしていたときと同様、進捗報告、情報共有、トピック紹介など。
 基本的には変わっていないのですが、上記のような内容を一通り話し終えた後に雑談する時間が増えたように感じています。

 「雑談」ですが、これは決して悪いことではないと思っています。
 リモートワークが始まり、食事や飲み会にも以前ほど気軽に行けなくなったこの状況で、誰かと他愛のない話をする時間が圧倒的に減りました。
 そんな中、週1回でもそういった雑談が気軽にできる時間があると、かなりストレスが軽減されると感じています。QA定例はカメラONで参加することが多く、画面越しとはいえ互いに顔を見ながら話せているため、よりその効果を感じられるのかもしれません。

f:id:tkgnori:20201217164208p:plain
作業に集中していると時間を忘れがちなので、開始直前にSlackでリマインド

各プロジェクトの情報共有の強化

 社内でほぼ同時期に新しいプロジェクトがいくつも発足していった時期がありました。
 当時はQAメンバーが誰も関わっていないプロジェクトもあり、何をやるプロジェクトなのかがよくわからない・いつ頃QAリソースが必要になるのかが不明……などのちょっとした混乱が起きていました。
 そこで、対象のプロジェクトや、それに近しいプロジェクトに参加しているQAチームのメンバーを中心に、各プロジェクトの動きをウォッチして何かわかれば定例で共有しよう、という動きが活性化していきました。
 QAチームのメンバーは各々別のプロジェクトを担当しているため、以前からそういった情報共有はなされていましたが、その意識がより一層強くなったように感じています。

リソースの可視化・管理

 これは変わったことというよりは新しく始めたことですが、QAチーム内で「誰が・どんなタスクを・いつからいつまで抱えているのか」ということを可視化して管理できるようにしました(7月に入社した福富さんの考案です。ありがとうございます!)。
 人手が足りない時にチーム内でヘルプを募集したり、QA依頼がスポットで発生した際に誰が対応するか、というのを定例で決めたりするのですが、その際に「〇〇さんはこの週忙しそう」「来週から少し手が空きそうなので私が引き受けますね」などのやり取りがスムーズに行われるようになりました。

 チーム全体の状況を確認できるだけでなく、自分の抱えているタスクやスケジュールを随時チェックできるので、自己管理に役立つのもポイントです。

f:id:tkgnori:20201217154833p:plain
チーム内のリソース管理表。スプレッドシートを利用しています

兼務状態の緩和

 これは単純にメンバーが増えたためです。「QAチームの人数 < プロダクトの数」なので、元々1人で複数プロダクトを担当する状態が続いていました。
 それは現在も変わらないのですが、1人あたりの担当プロダクト数が減り、負担が軽減されました 🎉

これから変えていきたいこと

チームとしての目標を決める

 今年は、トレタのQAチームとしての目標設定は行っていません。
 以前チーム目標を立てようとしたことはあるのですが、「所属プロジェクトごとに文化や進め方が違いすぎて共通の目標が立てにくい」という理由で諦める結果になりました。
 当時と現在とでは状況がかなり変化しているので、来期に再度話し合いの場を設ける予定です。

属人化対策

 各プロジェクトに所属するQAメンバーは基本1人、そして同じメンバーが長く担当することになるため、どうしても「その人しか知らない情報・仕様やテスト方法などの知識」が増えてきます。
 今のところはメンバー全員元気に働いていますが、いつ何が起こるかは誰にもわかりません。
 万が一のことを考え、なるべく分散しておくのが理想ですが……複数人で1プロジェクトを担当するのは様々な面で負担が増えるのは明らかです。
 積極的にドキュメント化するのも一つの手ですが、メンテナンスを続ける強い意志と時間が必要です。
 このあたりも徐々に考えていきたいですね。

おわりに

 QAチームだけでなく、トレタは日々変化しています。一緒に働く仲間も募集中です!

 また、トレタについてもっと知りたい!という方は、オープン社内報や、トレタで働く人や文化について発信しているnoteもぜひご覧ください。 note.com note.com

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

本記事はトレタ 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もその一つです。 興味ある方はぜひ遊びに来てください。

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

© Toreta, Inc.

Powered by Hatena Blog