ねこものがたり

いちにちいっぽ

『はじめて学ぶソフトウェアのテスト技法』を読みました

bookplus.nikkei.com

読んだ経緯

業務上、誰かの開発した内容をQAする機会があります。

メインのユースケースやよくあるCRUD的な確認だけならささーっとテストケースも列挙できてQAもすぐに終わります。 しかし、お客さんに提供できるかどうか、スムーズに使ってもらえるかというのを確認する工程なので、もうちょっと突っ込んで「良いものができました」と言いたい。し、バグなんか絶対出したくない。そう考えると「基本的には問題なかったけど、本当に大丈夫かなーって思ってる状態でQA終わりましたって言いづらい!とにかくあれこれ試してみよう。よくわからんけど!」となったりしていました。

その結果、たくさんの変数を考慮した膨大なテストケース苦しんだり、手元にチェック観点を用意してもそうではなく何気なく触った操作の方が(割と重大な)バグが出てくるなど場当たり的になんとかなったりしていることに限界を感じていました。

つまり「QAするにも戦略・戦術が必要」という課題感がありました。そこで、どういうテストの進め方があるのかを知ろうと思って本書を手に取りました。

感想

テスト技法も銀の弾丸はないのだな、と思いました。 しかし、単体で銀の弾丸にはならなくとも、複数の手法・観点を組み合わせたり使い分けたりすることで「最小のコストで最大の成果を出す」ことは可能なのだろうという手応えを覚えました。

また、本書で紹介されている一連のテスト技法は「テストにしか使えない技法」なのではなくて、要件定義や実装時の(大小様々な規模の)設計時に使うことができるものだとも思いました。 どの技法でも「こういう結果になるはず」というものを明らかにする作業がありますが、単純な例で言うと「AかBならばA’ということだけ決まっている。ではAでもBでもないときはどうなるか」のような感じです。

学びや気づきがあり面白かったですが、あまり理解できなくて読み流してしまう箇所もちらほらあってまだ浅くしか読み進められなかった感じもあるので実践と読みかえしを交互にやっていきたいと思う一冊でした。

JSで日本語文字列の「何文字目」を扱う時はIntl.Segmenterが便利

この記事のきっかけ

仕事中、Next.jsで描画する画面で「ある文字列の20文字目までは表示したい、21文字目以降は '...' で省略したい」という仕様がありました。 こういう要件は度々あって、すでにプロジェクト内には String.prototype.substring()を利用したそれ用の関数が用意されています。しかし、String.prototype.substring()ってどんな文字で期待通りに動くのかなとふと疑問になって調べていくうちに、やはりうまく動かない場合があること、Intl.Segmenterがに合わせた処理ができることを知ったので書き留めておきます。

String.prototype.substring()

String.prototype.substring()の詳しいことはMDNに任せるとして、例えばこんなふうに使うことができます。

const str = "こんにちは"
// UTF-16のcode unit単位で、strの0番目から1番目までを表示する
console.log(str.substring(0, 1))
// => 'こ'

String.prototype.substring() で困ったこと

基準がUTF-16のcode unitなので、1文字で2ユニット分ある文字だと人間の直感的な期待値と戻り値が変わってきます。以下のように、ベースとなる文字とは異なる文字が戻り値に含まれていたり、文字化けのようになったりします。

const str = "𩸽は美味しい"
console.log(str.substring(0, 1)) // => �
console.log(str.substring(0, 2)) // => 𩸽

絵文字でも同じことが起こります。(絵文字がコードブロック等では文字化けしてしまうのでスクショ貼っておきます)

Intl.Segmenterを利用する

この問題を解決するにはIntl.Segmenterがあるようです。

MDNでのIntl.Segmenter() constructorの説明に

The Intl.Segmenter object enables locale-sensitive text segmentation, enabling you to get meaningful items (graphemes, words or sentences) from a string.

とあるように、オプションでロケールを渡すことができて、そのロケールに合わせて文字列をセグメントに分割したオブジェクトを生成してくれるのがIntl.Segmenterです。

インスタンスメソッドIntl.Segmenter.prototype.segment()に処理したい文字列を渡すと、分割した結果を得ることができます。

const str = "𩸽は美味しい"

const segmenter = new Intl.Segmenter('Ja-jp', { granularity: 'grapheme' }); // 人間から見た1文字ずつで処理
console.table(Array.from(segmenter.segment(str)))

出力は次のようになります。

(index ) segment index input
0 𩸽 0 𩸽は美味しい
1 2 𩸽は美味しい
2 3 𩸽は美味しい
3 4 𩸽は美味しい
4 5 𩸽は美味しい
5 6 𩸽は美味しい

このテーブルの(index)がArrayとしての順番で、Intl.Segmenterが持っている方のindexはコードユニットでカウントした時に何番目かという値になっているため、「人間が見たときの2文字目」を取得したい場合はArray.from(segmenter.segment(str))[1]で取得できました!

また絵文字も問題なく動きそうです。(👨‍👩‍👧‍👧がコードブロック等では文字化けしてしまうのでスクショ貼っておきます)

その他

本題から逸れるのでこのエントリからは書いてないんですが、MDNのIntlのページを読んでいると、色々な工夫や「そんなふうに使うといいのか」というものがあって読んでいて楽しいです。(別件で、西暦を和暦に変換したい時にもIntlにお世話になりました)