テストデータのバリエーション増やしのためにまずはテストデータを減らす、の巻

こういうのに心当たりがある人は、まぁいつも「ちゃんとした」テストを書こうとする人なんだろうなと思う。

ホワイトボックスであれブラックボックスであれ、「入力のバリエーション」を生成するためには、「機械的な規則でパターンを網羅する」のと、もう一つ、ワタシがやってるようなヤツだと「本物データ」がすぐに手に入るので、「手っ取り早く本物データを持ってきちゃえ」の2つがあって、まぁどっちもやるわけだね。

で、「本物データ」を使うと、すぐに現実にフィット出来る代わりに、「動機付け(理論付け)のない収集」になるので、「網羅してんの、してへんの」なんてのはもうわからない。「なんかすごそー」と思うだけのことだ。(要するに見掛け倒し。)

というわけで、最初はとにかくインチキに「持ってるヤツ全部」でやってた。けど、「論理的なパターンとしては全く一緒のもの」が大量に収集出来てしまっているので、「網羅できてない可能性が高い上に滅茶苦茶量が多い」という、大層無駄なことになる。

テスト対象のコードはこんな感じ:

 1 MalUrlBuilder.prototype.calc_cname = function(name, cname, context) {
 2     if (cname) {
 3         return cname;
 4     }
 5     // if cname is undefined, cname can be calculated by name.
 6     function _conv(s) {
 7         return s
 8             .replace(new RegExp("[: +/=]", "g"), "_")
 9             .replace(/&/g, "_")
10             .replace(/"/g, "")
11             // TODO: "&", "#", ";"
12             .replace(/&#[0-9]+;/g, "")
13             .replace(/[!\[\].,()@?*%~$]/g, "")
14         ;
15     }
16     var res;
17     if (context == "anime") {
18         res =  _conv(name);
19     } else {
20         var spl = name.split(", ");
21         if (spl.length == 1 || spl.length > 2) {
22             res =  _conv(name);
23         } else {
24             // "Arisugawa, Otome" to "Otome_Arisugawa"
25             res =  _conv(spl[1]) + "_" + _conv(spl[0]);
26         }
27     }
28     return res;
29 }

「name」から「link canonical name」を計算しているわけなんだけれど、この「変形」のバリエーションが結構あるわけよね。この生成仕様から手作りでデータを作ったほうが早いことも多いんだけど、まぁ「持ってるしなぁ」てこともあるし、それに「本当の本物にフィット出来なくなる」のは困る(つまり「現実」に対してテスト出来ている、という自信の方を失う)というのもあって、実際のデータを使ってた。こんなテストデータにしていた:

(name, expected_canonical_name) のペアを管理している
 1 {
 2   "anime": [
 3     [
 4       "Accel World: Infinite∞Burst",
 5       "Accel_World__Infinite∞Burst"
 6     ],
 7     [
 8       "Active Raid: Kidou Kyoushuushitsu Dai Hachi Gakari 2nd",
 9       "Active_Raid__Kidou_Kyoushuushitsu_Dai_Hachi_Gakari_2nd"
10     ],
11     [
12       "Active Raid: Kidou Kyoushuushitsu Dai Hachi Gakari",
13       "Active_Raid__Kidou_Kyoushuushitsu_Dai_Hachi_Gakari"
14     ],
15     [
16       "Ai Tenshi Densetsu Wedding Peach DX",
17       "Ai_Tenshi_Densetsu_Wedding_Peach_DX"
18     ],
19     [
20       "Ai Tenshi Densetsu Wedding Peach",
21       "Ai_Tenshi_Densetsu_Wedding_Peach"
22     ],
23     [
24       "Aikatsu! Movie",
25       "Aikatsu_Movie"
26     ],
27     [
28       "Aikatsu! Music Award: Minna de Shou wo MoracchaimaShow!",
29       "Aikatsu_Music_Award__Minna_de_Shou_wo_MoracchaimaShow"
30     ],
31     [
32       "Aikatsu!",
33       "Aikatsu"
34     ],
35     [
36       "Aikatsu!: Nerawareta Mahou no Aikatsu! Card",
37       "Aikatsu__Nerawareta_Mahou_no_Aikatsu_Card"
38     ],
39     [
40       "Air Master",
41       "Air_Master"
42     ],
43     // ...
44     [
45       "3-gatsu no Lion",
46       "3-gatsu_no_Lion"
47     ]
48   ],
49   "not anime": [
50     [
51       "Aculeasis",
52       "Aculeasis"
53     ],
54     [
55       "Adachi, Tooru",
56       "Tooru_Adachi"
57     ],
58     [
59       "Aida, Rikako",
60       "Rikako_Aida"
61     ],
62     [
63       "Aihara",
64       "Aihara"
65     ],
66     [
67       "Aikawa, Chocolat",
68       "Chocolat_Aikawa"
69     ],
70     [
71       "Akagami no Shirayuki-hime",
72       "Akagami_no_Shirayuki-hime"
73     ],
74     [
75       "Akagi, Miria",
76       "Miria_Akagi"
77     ],
78     // ...
79   ]
80 }

ひとめで無駄だとわかるわけである。実際かなりのデータ量なので、読むのも非常に疲れるし、もしもここから「手作業で」刈り込みを行うと考えると、もうヘコみますわ。

というわけで、「そんじゃぁ、ここから意味のあるバリエーションだけ抜き出しちゃいますか」と。「網羅してないかもしれんぞ」は削ったあとで考えましょうか、つぅわけだわな。

javascript なアプリケーションなので node.js ワールドのままやってもいいんだけれども、javascript はループの記述がダルかったり、必要インフラが色々欠けてたりで面倒くさいので、うーん、Python の Counter 活用してやっちまおうかと。まぁ Python にしたらしたで今度はエンコーディングの問題が鬱陶しかったりもするけど、そんなんワタシは慣れてるのでどうでもいい。

というわけで:

 1 # -*- coding: utf-8 -*-
 2 import io
 3 import json
 4 import string
 5 from collections import Counter
 6 
 7 fn = "known_cnames.json"  # 元のデータ
 8 ofn = "known_cnames-reduced.json"  # 削減後データ
 9 
10 data = json.loads(io.open(fn, encoding="utf-8").read())
11 newdata = {"anime": [], "not anime": []}
12 for k in ["anime", "not anime"]:
13     dejav = set()  # 同一パターン管理の set
14     for row in data[k]:
15         cnter = Counter()
16         # Counter で管理したいのは「ascii 文字がいるかどうか」と「非 ascii なら
17         # 各々の出現回数」。つまり、「abc def」と「a bcdef」は同一パターン、
18         # 「ab c def」は新パターンとして認識する。同じ要領で、「ab-c」が初なら
19         # 「ascii がいて、ハイフンが一個」というパターンとして認識する。
20         for c in row[0]:
21             if c in string.ascii_letters:
22                 cnter["ascii"] = 1  # manage just whether if exists or not
23             else:
24                 cnter[c] += 1
25         djk = repr([(k, cnter[k]) for k in sorted(cnter.keys())])
26         if djk not in dejav:
27             dejav.add(djk)
28             newdata[k].append(row)
29 
30 with io.open(ofn, "wb") as fo:
31     dsredtowr = json.dumps(
32         newdata, indent=2, ensure_ascii=False).replace("\r", "").encode("utf-8")
33     fo.write(dsredtowr)

唯一ダルかったのが hashable の件だけね。あと Python 3 だと辞書の文字列表現が実行のたんびに揺れるんで、そこだけがダルいちゃぁダルイ。たぶん10分くらいで作った。

で、うん、1/4 に減った。まぁまずはこんなもんかな。

なお、「非 ASCII の各コードごとの出現数」としているのは、"Aikatsu! Music Award: Minna de Shou wo MoracchaimaShow!" みたいなのが人間にはわかりにくい振る舞いなんだよね。一番上のテスト対象コードを注意深く読んでもらうとちょっとわかるかも。「_」に「変換したりしなかったりする」というデザインらしくて、結構キワドい。


誰のためにこれを書いたかって? そりゃぁもちろん「オレのため」が第一である。上のデータ削減コードは、もう捨てる。だってもう使わないもん。だから捨てる前にここの載せとく、てのが第一。

もう一つはやはり「魔法なんてない」ということを知って欲しい、ということだわな。ずーっと後輩と接して思ってたことなんだけど、若い子はなぜだか「何かステキな魔法があるに違いない」と考えがちなんだよね。だけどね、「なんであれものづくりをする際には、血と汗をかく」ということを、「誰もが」やってるんだよ、ということ。それを人に見せる見せないが人によって違うだけのことであって、皆が見えないところでこういう地道なことを、コツコツやっておるのです。



Related Posts