Elm
elm-test
Elm 2Day 4

[Elm] Fuzzer でテストデータを量産しよう

More than 1 year has passed since last update.

Elm2 アドベントカレンダー 2017 の4日目です。

elm-test に搭載されている機能 Fuzzer を紹介します。

Fuzzer とは

Fuzzer とは elm-test に標準で付いてくるテストデータの自動生成機能です。 Fuzz モジュールによってこの機能が提供されています。

公式の例を引用します。これは「任意の文字列に対して2回 String.reverse を適用すると元の文字列に戻る」というテストです。

import Expect
import Test exposing (Test)
import Fuzz exposing (..)

suite : Test
suite =
    describe "The String module"
        [ describe "String.reverse"
            -- fuzz runs the test 100 times with randomly-generated inputs!
            [ fuzz string "restores the original string if you run it again" <|
                \randomlyGeneratedString ->
                    randomlyGeneratedString
                        |> String.reverse
                        |> String.reverse
                        |> Expect.equal randomlyGeneratedString
            ]
        ]

コメントによると「ランダムなインプットを使って 100 回テストを実行した」と言っています。どんなインプットが生成されているのか気になるので、ここで Debug.log を挟んで確認してみます。

                    randomlyGeneratedString
+                       |> Debug.log "str"
                        |> String.reverse
                        |> String.reverse
                        |> Expect.equal randomlyGeneratedString

すると ↓ こういうものが出てきます。

str: "o^i:WCk%LQ1A7Nrmy/\\`C'/7ehRQSRHK:zBi`E|[iM<xpIA"
str: "qe}[8F?a#"
str: "W)Ov+c<44"
str: "4b1\\S-"
str: "oTd2"
str: "XXb1q'}Zv2}&gzItPe|cgRagI\\7Xn4j#<55SO:W*L]s"
str: "[;>rd;oz T1L|6?X<N0)w1wRc#DTV4ub^i`Ld!Eup2GV(nF>ai\\}Toy6J6e%'zjf Kl+eV\\v_EH<_4i\\L*m@`zTD3+wAJickX'V\"OKn\"|Iye?|Xs6}4uhv,RPr1Z]c\\5\\mD;]^}TB-yTtQleU!Rl[zMo5p;*)EH_1-OdWjRgo@.(H\\rnN'1$+`~9{&'_VL/PP#nZBYB+vL@*k:fpcX0Q3uPd,U`I\"z3;[Bk/tRUYHr:e5}-wT1~uyi{hpeJ!Pgxtinq|\"A(Mk9;:^)4^'CfTvN@|;]\"B}Ed_p[5epoklqP:BC\\$RWtrY@v6p|mH*<G>~hw%F+$R1C2hD:X6(4V2+{ic_,0NmmkvUCBCio1X./gEW?7l=xf})-{0Px0&v/GwVz*/vI6Jgxtm/RJ%%85ie'Tt%KmyI4#>0ksCS=FYOp`kDm@jI%4ZV:\\ep^_pC|aY71m|i]M-.?+JvjpK#cGIg{jn/4N{@j)&>a`#p?t43qMPeb'{(=^r,R>f\"w.(#[TR;(9#p<A ^tSS4 1oCE3}E]n9'AI  M)Wtbgl"
str: "uBUv\"sng[F"
str: "$DMPyNMl>b8MdvcFU}@1"
str: "qkpoXY2yx"
str: "Bt;U4t=I"
str: "yudE0?.TSfHwNnm-?ZkcQdl|a|@Mwt0ggO(%#\\K"
str: ".8NUktI!]p"
str: "Cs^A ';V\\r*d>Evr?:L4z.>dm`Q6rt[G5_%:BwWN+[u+0W3((@L!*RbtEs8:eKkuvIaji|<bngC/]MP_32r!l#%B56X0J:~q>'.\"zU6vU+GB~a-H8STt%>'\\Say7NWQ>52%ZBwEOPY#>@8Z--^d;1+JF)E@ lEbP+wqhi9}FgOTNM 7CW#gn6nt D\".BWJ6?dI*~:`0GI!R0ARIOFPI#s/#k`5[8*jcpv{azSLI.$LckJxH8peiVW?7,a~^bM<:4'S,d/d@~07|*v>{kp)V.\\L@hCuV/,6V\\=z*T&7lqlzTFaydyZib-jGu`Wf1me=f9qSmg!.M&M0V%#jHIehpm:%_{dE2fsUCKpKYz A}q[ACD4R#92w/'a11PTp%T^+gA6 LU9:W_J&`MSp8]F$A*0n(_E4XgXkbb0o@+_]SvK&48v9D,wo2,@sr=`K::z^S:rrwda7F2lp4PyhsENbXfVIx0=0>h`VP/FE?8K*Cr\"=4g*iQfI ZGU)WvB+*y:r<eEdDKbYc+1mlU*Z*<*^(1_i\"M7\\]oZ^d1}T4j'd qWxa(0qI]EZ)Lm!rU\"g^KMAX@&;c>(Iqpc6qn8XWCV#Ba3rz[TQKQ,{py`Z`to'k,xkOfDFUlK~b,u4*=KZoE\"eF_Z?0`Z\\xw6BX!dDs\"zY#Wunmod#;SPOIQnkVxyf1[W:&fKl5,ReQ TjwQq^iVS$9Iw/r_),{}I^Y+e/sf0c40m%HYXOLA1X]!kv[;CO<x.gRJ+'}Usn\\5hrx-5gU5X\\B0SiD1MX@qO9'7#ylA}Q`{Z:HA=\\'H.NX4X?uW&#'!}U8r_.g3@oZoc&d* }6n5()V8gQV#OW$=S|[O_Qc.@4m16]\"<{fp;=dxmB*(3X="
str: "QUKIOb72!mMt52BS]$+K@+:,Ux5==Ke>ZJ#Bhk#HcQkNCtsY~P?|0\"mmW'vlt#LG]klh.RaIQZ9H+sw\\/@H|nqlS-y!-^/;%\"hHj{?fo{lN3)O.^{d+<,\\.xYPg3lpT0#>2jhOK[|fF^Vvbh~FQ2dP~dB)fXB'n[@uvkCumU(<2~.Tw.LHL\\@|Ia}gN &02[^w7/m2yHn[[;Ec*z)G-1$I0xCwJvicGC`-2rU4A{M=dk$e, xj@?M(8[{QXK6~eK9!=wF}x]6+7^\\5C)69X`\"nbN\\NQ){)lq~U8~-WXu~bS^!T_8MNW\\Pw/ JXLx-iNh@u,7!Z;T2vxPsqb+^|h`5`U=!:R_#*F%9;k!Bbp~YuxjHw,43 UUyQ&BIAY3/Yl|z>oqZUe$CiP>)=hr\"bDx>Lrl@--t^;Cs?<Go.9T}qz%-oQ9)e0BH0Ug%8-gd+mvNd7K[^#g1{P%C:+\"wu=<]5ie5W~mEpEK0u8R=0QCZUE^3p\\Shi+nk?bZ7l'2wf |%\"k`g%BAS?Nm&A&IcP-$\\W^I]-H'qG?[l<ICZyTZE?xvR5bNmg1PTjmiIIT8<Lar>%@aqL+T}q0s(Y%` a99[8U`]hbC&Gn&6iYcF75NI]dQ~~5}5*P]dywx.Z.DI>{ik^gO?$p8Y$V+)EGt[!fs}C;9N]uVXTPMu`6Bj_#S~"
str: "40 EB=:g\"e~\"oiAR{NFq>\\Y]g"
str: "B+h# Xo=M'[DYcsxtlKb=bgED2ZQ,bh6JAbU15LlDRDNb,dST;Bz%?v(~ lH%{7&$s8M5PNc[,Rc4OT{#Bkv#&Dt_!u73/6)OYDoLQ ef^g2kWcXeA`'d)'0/\"~5!{W]dD'hk<rj->uxSTC.|v2)`}Zc&9I4RARH2+aBpC6Ude$z+1_mMJ^U[{N8Ii&1`6ez3)idGe:KNh-=B\\6~S7OLTSzx?iIXP.X(u{&jHU&`)yxvl=o/m6/+N&{X.1nrfLwgCsh*OLoK*F[^>wh37}:h}2`zR5&JTV_#Pp,6`e?INb]q_7bv2v!ex']Y3Rt)(wJQAnwFSgzh]Y(e"
str: " \t\t\t\n \n\n"
str: "Re8b3kqY#"
str: "Zith_RY4!uFidYq<Z AHII.G,}Q6?&n+,RC&3X8tjND/DQJ?fH&#?gZbw<-kh 7?b;#nv"
str: "PeBuQ;"
str: "#[t0S!\"On`@r8b'?)?N|_tF@ebG\"NmZj+UL%qEqb){gn8pL+>b(Fg#z| [jn^#e~/@YKd?lcAFUH-#'181}bA({}7^]05=L$/OrA@^ZnT>v E{st\"x@vjHF9a'IsupU_]w^ (So'>QY:qa&5N8@RG0C#^yr_[<E7{FOVA=CF4zwg<M5I44x>\\ #bd 1Le:UiNTc& <hZP#$M}Qr^cE}$;A Q6}(L<Y+EF}\\1b_#+qo-hX^3R[[]`%]<6@o;mWO9,$871{a%TTr'#F35$.uG\\t+;M~O*M=/8Gwe@jSVe~@3rxU>za~8USs~}V?.|8[<*~/IVD7$LHJ%L"
str: "y6"
str: "9UZYrnRM."
str: "%29|r(p<HXgDKqY4x:w<8l\\E>#49H^&uJV6,pzwe6p\\"
str: "3{UnA"
str: "H"
str: "?q;r!oy"
str: "VkT"
str: "JNw;)j}(UYM11y}1|cOq*pv{<hW*Po.{5M'AHZBsp=35[K'+?rVWR=2F/G2zOeaqD.4#>}pOt;T}Kp"
str: "j_0 'Vzm5A*E~9ejON2\\t{+c^sjx~F[s.dblZ|DdF#cA8K??+4GRRj88gnOaf0#)I#h|E)PA&Q^WC;ov1rl7/OSzPQ[@@Bs.+V(jw6u.\"K:<bQ8eW{\\\\Xz:8X6I)D<Tt2)-?*27<xEl$Fvf{'p0h^@3^X~^%99>HIDF)YK~#\"3nybI(UAEcUtISQ})\"2^*M3s?\\ci1JrV]]s83[w:C|A1&=17iCIx}F_X9&]1b-K'|a=(SU}PL[F6TI2\"=!i1_uX|N<:?%81j3^}l'V@EES>9y}N"
str: ""
str: "W]W;?l"
str: "\\"
str: "N U^nX2"
str: "r*|\\iUfU"
str: "4/8?YqY@G,%~pau\"'"
str: "rT@tdxnwC[s-Y'},3VX9Gk4g\"CfV$jr1*z9G^|j{]vz)tr~vvV7c2Ph)<WuK4BM7,5VyjunJWhRC!!T]y-N0/Xd!'m4mw5>d%]@~E:tO_)f JH0=o\")6'}K: q4zw%>&2fz`IH;M<W`.W)-B#LRx`IQp=_%|#FGnO,EnUP@!t=Pjl,lE4iM2K8pm$+ta.@`L0E\\{=-0Q}!J=h\"RzAVJ#8SfAqUbc&).8D,V3eg&}^2OT;Do(|\"|B[u,+lGE-2Vd'oP*pN'CLD4Uo8xW)']&ocY_Z~!1Z-GJW9cC+2aw&EDW=6<nr75@v.,h@#(?wPgQ-+'r8J<&nHLLF~[ZdWTEx\"9>Y8W'#DSD>Zl[nH'`8{#5oQ]Psm|}|*74Jom{HDnAfJD 28`9)&|QW\\.>*=rrtap>E5e=-iOGUwP3@eFoj5-G%Cl[wE{YSg~w0$MJbuY_j8W>d(?2LQGi>n@KDlt5-:AYhX|!yNQY|`dv\"Bky:QY5jilQj+lro%\\ymZ`UdDJ;8'MMR])83<cX~q%rCX38 qGeR\"OD~*]2]qQ?{Re[|jkUs>aR)}c+wG=^MwM)n=N?~A$Nh-j8HiO,C<7ym\"xF#ru<+>|Z1jf,<<d&4W3{s/XQP3NxV4/~n[:$1!8uk9 Q>~{sRRV.zX,"
str: "'}|-4-"
str: "tf;l"
str: "zI]1<9($sm"
str: "f9FK<(~9hCB'P!q(*gJpQq0:m6a&_XSrku_"
str: "oud>[P"
str: "I"
str: "J"
str: "Qe@"
str: ")"
str: "\t"
str: "/LnVr=q,"
str: "^xK0Q.3Ej*;<O}"
str: "k1C#dcn-2=v`xI,q.NmwRDk5wY9QE{IR|z~hG6V1dO[Jl]*/f=)C^Z=<i=V+<{ ]le@Cpy{KD*47[gX7(-g?}f2"
str: "X!l+.d6_E)p\\m6|&5L3;?9qK]P<6RfH? lo6n*harXJv\\uH &8O7~h-UJJ(g7\"]*|o*b}zvw[0Z)-tcJ<`m`U?Fa:r6iX?VAqbfokyY7{_7w~#);Opj !xwXW8G~$I^|qr/9)ZXV-Vrm`k9HOHR[w9V<Cfhsn7dmk2/Dov=L.4g\\&o-9G55?FU+UAF&DzdxA Z#Rh!``\"{+xKE:vxs@*C}gt45Sn;2aD_HHaZP9ju#pm/n8R^')Cq8#0^;PdonpS_doLV4Q8:y=+^\\iPl-|9/'!HHkJl^lXvW14}FJ=6>[Ri!EkMuT_&Vet_*pl^}!a&!v wd uWP;2I}e`bZ[5oFmw}|_05v]mo*]_]$wZC<}k.G3i[QWcD>~xfRP;@'\\yvw7Ud*RNpT?e]x1qjP:FV5*OD-oEHcw|xY6HSV/znLu.+1{1uc(r7eU_T[CTx_z0<d-[bE1+kp#&}?f.XPYK~OZ>*HCs:OA:_|u< +X&@C<&Y?wRNX'9.nzsCkD'5qp8, =UKfn0*xVM,aK_(4?C/U$ox>c=;x[W^!|r/Y.ze:#OaJp<:fNguDAB-=k.r70&}+16oi5t+B\\>t-@o\\;C(ZH.kO9#5L!.![xChk<'{ikWa>#IJo9QN,HTtuM?[Pz4zhxA!d_Y-JUPS|Xb4~f_CPlc fI]agL"
str: "Eob.=~(I"
str: "Luhw^5<`aa^Xo_"
str: "=4iFi-e"
str: "iN5ZuN7J;e"
str: "1'bYP+?so/F-B 0XRoYFhRla,0m:/I%+gc>IsrX&I=2OsU5O!c:wHGLn8$4cZ/F8wQI2g(v`G6t~w82v=K[K/Po:m+,y5nHMj#kL:_CzI LVkA=9]s`=@Ek+(GlK7s)RHtaW&!C?_}r1`?\"_!bKv2Gv?A]ls]I]##?8"
str: "<b!;"
str: "W}!hW"
str: " k=Yz"
str: "5:cX>ngAXs9L!JH'q8<p?\\q*0!:xFFb0G!NH*/q$~t1Iegvz@sUAh'WK21QYDZ&Wm$(#?jA=,/\"zd*3-"
str: "j\\Pz&sO"
str: ""
str: "Z'}2"
str: "UnS=5*lcCfWC4L 0K8Y9vM^1^5fs{G2xjp50_F=2`9&Apv?s,dqCA|Pl19bh73~)w{{ OzzTVU$l.svq>#ivERy$FoG$\"cT.By'Tjv18]6Q/]g|hHJH6}#@?),zP,u)VG9P*L}-IB7wMk%@jC5Z~Ob{O(;Gje.v}~h3K?&|m%#f~*CcF:&DY6>$fY/UvRs^7O<Xdo;c:H&STYn?4q?0,7tzXR/2h&QV&21ofg=,.S*yF`<vY.SpFw[S[1F^-?hc5/@Rp3^-0{K6Kq(uX2`uNft@+L==#h]H-LUk%qu}MTXyg6sVt\\=1Ygf&#KO5/kZld$3it2]u~T.ICsdn*0%\"Z4TFePjxgntW-:8fVDs^+j3yz4QrD;IJ~9r|+L+8ri@=D!ay,D!FOdpL0-O@q(wA.2z0)[T+UW[Vjo^,Tn~57\\v,0d)}}#2\"\"E>JW]_)M|t\"*OF)z|8ay{UN%@v^WuGhJ{<my8=8_p#e{>gB8!t#QT'k+k|6l/._pDk 7eypvx3!lK`W)_B1$|PjP.(!gsjMN!:SD)fqt@eZBvLbAVEf69R0X~|fs+'M|,3Fd%<\\;qf$-]?aQ~x4hNx,65!cP~7#dt=,pS%r^P<;*QqG;8 -qMY-<y*&I-D@poRM>j_/r|r,A|pA@B-lUbAZdBb&T0FQRl|l^JUV%cIsRfDv\"BZrw)_'P\"YF\"f;&-,l}}@0\"F`YdUO/E\"|On\\03pO<F9 qE^'Y\"V''wmjw`(idH)0&aAn_b\"?uQUYcFHzRm9 '_5\\Il*\\chj?1:K8Q\\tF=Q]i+=+}HO0xwyQ<([+jQnmO^@#} oK08`X8xA<e**501osPUDI!=rk^P$i=yut~2WFC!op1d#JGMHg>YPj(2*zZ-:/mhD%</ZWM"
str: "?:Q/'F_&}X"
str: "nQ\\d2"
str: "   \t\n\n\n\n"
str: "KN?8zP"
str: "rh\"$}"
str: "vpiJd;{-.`&yi0O*!OA43wJl|tGr.bZIMML"
str: "'*5s"
str: "DPkTweqMH"
str: "` RK::RJ+Tr$V-Sp}Gol$\"}}VT 4;oMpb$[~E? M[`I3v1/9IM)fl7|J2pFx/NiL ,XgMzEc-1|h=v5[2/Ij;YNt5s4 pfz>)dRbXb#@#66Rv|p86bIM&t(i,BlY=6}=n.=wRRux>-I&PETd*2?'[9Rz*w7}mF^Eu$%ExUQl@t*QBjEV5T:#V=&)zUqs\\TDV\\bVz7BAQ0m}Qk+kdB#.x<h)^b3Ar\\V|IJ'Wd{K=EdZ1+s+0V@_ aHiszUET~g=A~)|@Qbe:%x(AjPXgKL^F1]L'l6\\?y4-yk,cbvPx$!9$qf/Lr/NJIT\\nUWXP ObWZWw$*|m'\\Nc=(vm`TdR.b-SDi)x\"?Dv2b *Q]q^#i}O/ZjynO?Q=@i!,anQbZi\\6$xuj=3HCW?w=9~.X5g&ut7WE?sX?/M8z$}umLL\"+%\\w3tv/vw2H%rkgI Gm?79I&_}CIgleW\\'0qXO>S>21BjgIJp!N,M\\QG0hXLk#`AC~@[J?hqV:Pb-o@6h^nBR09%IYjVtbI75"
str: "Vw];MI"
str: "\n"
str: "e"
str: "\n\t "
str: "EA<70@!P~o(0."
str: "xc"
str: "kV!,"
str: "5cCj=p'M-m/vc}eqs^m7Dh-t6q~86/j< W/*.75r#GLW$rw$C#yN{1f2zd. QH~-z=<TM^Ek^|B_x$t@tF%|3$8&VU'm:p`M#%yoR,K\"8/T9X;@9.mS7aPg-.cE}9#qZ@A_ktw+'>U`iy(x<M?=$TIjCw.dfaa~<\"[=O$/lQ_k\"+y!G$;_$BE4,uw#u!-k1~iw7Es,jWrPs{S19d#ej21@a=a9 EO$u!WT;9/H0W&?DSqyTK(h>5WnI{"
str: "Ro"
str: ""
str: "40^?X8G0|$"
str: "\t\t  \t\t\t\t"
str: "1oW!K$07cM+sFeB?0hdhDt]2|wmHH6_a4mNXn?.M~SFY?QMfXg]e\\;X|eCTIi#6_8_ld`'L7d9`[C4;{brk+gTY]rI[t#[kiql(xfmiBrPqT[*%)OS\"I-Wn12b|c/jE;i2[[KKUG`;S\"p*E7c=Xk8Gb\"yy@,nqm_H&P$?=]m06Zsfh8x.3f)Lf4?,tJ,I&go4HRvc;ywQ31q.)>joQVS-4\\<!iy|Opyv{Yt8RV)z?ek:=c#n1t~omE/FQH -C{^z#d'I'xrMy`)%H(.s`4x$P_[C9c]=Oj~}$&$5{q=qP#qJ1gF;KRe2ma~i3Ksq\"r-*i:&Ya/,M=98PKC.6g\"y^+T'%HR>.WnGn\"-A[)/W9bj\"U-Z@<pTc\"D:vPeWc0V_m_v3Y]>{L[Lor4'j=zrX}Asp9/w5a@vIEq,K)6qEv[;@{=^Vpx=Rs.0^]##yWpjj_SjU_\\$8C/Q_c(uI}RExdHSAwtpGR@l%p<Lh3rCQ5o&)ueG*L0Q$(db>]!1`:7NHq_sh&/lf^8<Ts+#>@@piOpeh{UO?*d@Z6bTU/KSewW=D;`k%8_WW\\o1J|]y\"qAXE>JRr`83fa`z^{CR4|EwVOJ (#%X!9XiS5?9H'dg,|SHQPf(F0psRvRqw'.0a\"9*>fisZ7fP}lY)%AF=yX$eM=6,:6Bl>:5$S$)t=La2?LP{YgF0vYz;:Xx.$Awg911GPoiMB1B7K}J,_blz\\ynA@#|.(zMl>'Au.Xo8j(TKPiO30&nUWj/*v41wP`a`*Lo@uOK];IFb\\sy0ZQ5$nkA6b]$6UreZTNv}Ry*T^7Ay?{^OS$8(hvtS'avrOc&8lof%4IDnf6G)<8?=o$[ae<t"
str: "%(^7"
str: ""
str: "e[IVivk6nn[X2-QaG3iE7[o(6-6(`E4[[fW=Lt`'@Y#6(f`x]IWYjTK3SMHnzB(qR=(iEk1\"g[(=D53^`TQ4^/1~F)gCq]\";=ih1l(k2j#=#g\\~vZ\"I`r60>{oDUv7IkfSWg,A}ah|7? S6g$~9x3$+$S)_-+?W,J[6}ad>X;Fo3Oc+:%,J`2E}[/p+- Q0aLDA6;YSrG\\pkAc0SqUZ!#f~33 VlW_g5tM5BiJgt|{Z\\*V<QpG$;e5B}:8DS@G&!<W\"zz>53behpJpA'aAxZ' 5q*@9x@'JR*W`bOe1H(:jrf6RU_zI>VXW(Y$t0aow)w&VKJIy\\*)WVVSzC)WgSlQmo&9HP4Qd3s1EL5d0[;MlzcEZO\\k#RllI-/'@j&S;G4n^X0*2_xpL9oAw`?FRuNumL|uNeA45TG4(U2_Qw$V!arGE:H'6Zq/\\#P=V^p(~w9~z|M_uR8M{3>]Eh/(-a=?^4Q9dF$9v)hUjS.5aOyAe={!@S$[H4e?|!]aP^wDH7]<Ypy4lE6lu8f^@v$`xkgew%d=M9F?9HM[  nfoys!3LwVJb{K+{bVUd-eVD!#.DpQ;do\"u##XK"
str: "RqMXsS$"
str: "M<QzT3j9+8\\Av;@$hZ2"
str: "SPz"
str: "2'jlZjY=Zax (9e_f=95]$+<=WU-*^2\\=r#1_T)MD3UBi`NSIPj\\kDH`RP]y2W} m#q)PfzqY*<])sf*s=qftQ!/jV<ZhW6sna\\OkD$>3SN~:Q*6<',)8I^C0K$ET|/z]uWLY0*.TytuDDo{0^GuhfUsJ@+WOOd[YIeT!+8XDSN5`!Ei[GSsD.#@9L/+Ruj!x(WK9z\"vXvrk'@\"%f^ksC*H_+ISPOy%brdeN7w|?o_,|j?ZPZ?-xG<.Jwc$4Te\"2VfNxL`8O*j$k?WYvvrr]QQsRp6S9m:ym@#gkvFN6@U{<mNTk`zTc$T@>K;c@cB.Pe{WS9b`A6,bc$>NSEov*mhrHqu3,vK3QryJvP"
str: "zN*8;"
str: "id4sMO3E:]6RMq%7IC +&WsUAMbFRp#TJyLLsYA%l#ZD.h"
str: "\\d!#;&"
str: ":Wjmr!^j"
str: "\n\n\n\n\t\n\t "

ドキュメント によると、バグを発見しやすいように意図的にエッジケースを生成しているのだそうです。確かに上の出力を見ると、空文字や記号や改行コードや十分長い文字列が含まれていますね。

使い方

fuzz 関数は次のような定義になっています。

--     fuzzer   -> テスト名 -> 期待する挙動        -> テストケース
fuzz : Fuzzer a -> String -> (a -> Expectation) -> Test

Fuzzer a 型は a 型の値をランダムに生成する Fuzzer です。例えば、上で使っている Fuzz.string : Fuzzer StringString 型の値をランダムに生成します。

bool       : Fuzzer Bool
int        : Fuzzer Int
float      : Fuzzer Float
string     : Fuzzer String

生成する値の範囲を指定できるものもあります。以下は、例えば intRange 0 100 のように使います。

intRange   : Int -> Int -> Fuzzer Int
floatRange : Float -> Float -> Fuzzer Float

次に、Maybe やリストを生成する Fuzzer です。こちらは maybe stringlist int のように使います。

maybe      : Fuzzer a -> Fuzzer (Maybe a)
list       : Fuzzer a -> Fuzzer (List a)

Fuzzer の合成・変換

oneOf を使うと複数の Fuzzer を合成して新しい Fuzzer を作ることができます。

-- 0 ~ 3 と 7 ~ 9 の範囲で整数値
Fuzz.oneOf
    [ Fuzz.intRange 0 3
    , Fuzz.intRange 7 9
    ]

map を使うと生成された値を変換して新しい Fuzzer を作ることができます。

-- 0,2,4,6, ... 100
Fuzz.map (\i -> i * 2) (Fuzz.intRange 0 50)

map2 を使うと、生成された2つの値から別の値に変換して新しい Fuzzer を作ることができます。

type alias Position =
  { x : Float
  , y : Float
  }

Fuzz.map2 Position Fuzz.float Fuzz.float

もうお分かりかと思いますが、組み合わせ次第でどんな値でも生成することができます。

type alias Person =
    { name : String
    , age : Int
    , mail : Maybe String
    }


person : Fuzzer Person
person =
    map3 Person name age mail


name : Fuzzer String
name =
    map2 (\a b -> a ++ " " ++ b)
        (oneOf [ constant "田代", constant "山野", constant "柿沼" ])
        (oneOf [ constant "浩史", constant "義弘", constant "涼子", constant "彩菜" ])


age : Fuzzer Int
age =
    intRange 0 100


mail : Fuzzer (Maybe String)
mail =
    maybe <|
        map2 (\a b -> a ++ "@" ++ b)
            (oneOf [ constant "foo", constant "bar" ])
            (oneOf [ constant "example.com", constant "example2.com" ])

これで無限に人造人間を生成できますね!

person: { name = "山野 浩史", age = 36, mail = Just "bar@example.com" }
person: { name = "田代 彩菜", age = 0, mail = Just "foo@example.com" }
person: { name = "柿沼 浩史", age = 96, mail = Just "foo@example.com" }
person: { name = "田代 涼子", age = 97, mail = Just "bar@example.com" }
person: { name = "柿沼 涼子", age = 82, mail = Just "bar@example2.com" }
person: { name = "柿沼 彩菜", age = 61, mail = Nothing }
person: { name = "山野 義弘", age = 0, mail = Just "bar@example.com" }
person: { name = "田代 涼子", age = 4, mail = Just "bar@example2.com" }
person: { name = "柿沼 涼子", age = 0, mail = Just "foo@example.com" }
person: { name = "山野 彩菜", age = 58, mail = Just "foo@example2.com" }
person: { name = "山野 義弘", age = 69, mail = Just "bar@example.com" }
person: { name = "山野 浩史", age = 52, mail = Just "foo@example.com" }
person: { name = "柿沼 浩史", age = 59, mail = Just "foo@example2.com" }
person: { name = "田代 涼子", age = 61, mail = Nothing }
person: { name = "山野 義弘", age = 9, mail = Just "foo@example.com" }
person: { name = "山野 義弘", age = 0, mail = Just "foo@example.com" }
person: { name = "山野 涼子", age = 45, mail = Just "bar@example.com" }
person: { name = "柿沼 浩史", age = 66, mail = Just "bar@example.com" }
person: { name = "柿沼 義弘", age = 56, mail = Just "bar@example2.com" }
person: { name = "山野 浩史", age = 40, mail = Just "foo@example2.com" }
person: { name = "田代 涼子", age = 13, mail = Just "bar@example.com" }
person: { name = "柿沼 浩史", age = 97, mail = Just "bar@example.com" }
person: { name = "柿沼 涼子", age = 54, mail = Just "bar@example.com" }
person: { name = "山野 浩史", age = 17, mail = Just "foo@example.com" }
person: { name = "柿沼 涼子", age = 2, mail = Just "bar@example2.com" }
person: { name = "柿沼 彩菜", age = 0, mail = Just "bar@example2.com" }
person: { name = "山野 義弘", age = 5, mail = Just "bar@example2.com" }
person: { name = "山野 彩菜", age = 87, mail = Nothing }
person: { name = "田代 彩菜", age = 37, mail = Just "foo@example.com" }

Fuzzer の使いどころ

さて、ここまで読むとめちゃくちゃ便利そうですが、実際にテストを書いてみると意外と使いどころが難しかったりします。

例えば、足し算をする関数 add のテストを書くとこうなります。

  fuzz2 int int "addition" <|
    \a b -> Expect.equal (a + b) add

が、 a + b というロジックが間違っていたら話になりません。要するに普通のテストだと左の答えを定規にして右のロジックを検証できるのですが、

Expect.equal 確実な答え 不確実なロジックが導いた答え

左にも変数が入ってロジックになってくると、

Expect.equal より確実なロジックが導いた答え 不確実なロジックが導いた答え

となり、確実なロジックがあったら何故そっちを使わないのかという話になってきそうです。というわけで、有効なケースとして例えば、

  • 同じ答えを導くより高速なロジックをテストする
  • 逆変換して元に戻ることを確かめる
  • 別のロジックの組み合わせで実現できるかをテストする
  • 一つの答えを導く複数の値を生成する

といったものが考えられそうです(下図)。

  ┌────────┐      ┌────────┐      ┌────────────────┐       ┌───> a1 ────┐
  │ path1  ↓      │ path1  ↓      │     path1      ↓       ├───> a2 ────┤
  a        b      a        b      a                b      ─┼───> a3 ────┼─> b
  │ path2  ↑      ↑ path2  │      │ path2    path3 ↑       ├───> a4 ────┤
  └────────┘      └────────┘      └──────> c ──────┘       └──── a5 ────┘

他にも何かいい例があれば教えてください。あとは試してませんが、テストの本筋とは関係ないモックデータなども効率的に作れるかもしれません。

使ってみた

なんか意外と Fuzzer を使う機会少ないのでは…とテンションが落ちてきましたが、可能性は大いにありそうなので何はともあれ使ってみたいですよね。

というわけで、先日作った初心者向けの問題集「Elm ドリル」で実際に Fuzzer を使ってみました。やっていることは「答えのコードと学習者のコードに同じ値を突っ込んで一致することを確かめる」ということをしています。

module T1_Numbers exposing (..)

import A1_Numbers as A
import Expect exposing (Expectation)
import Fuzz exposing (..)
import Q1_Numbers as Q
import Test exposing (..)
import TestUtil exposing (testQA, testQA2)


suite : Test
suite =
    describe "Numbers"
        [ testQA "increment" int Q.increment A.increment
        , testQA "decrement" int Q.decrement A.decrement
        , testQA2 "multiply" float float Q.multiply A.multiply
        , testQA2 "divide" float (floatRange 1 1000000) Q.divide A.divide
        , testQA2 "divideInt" int int Q.divideInt A.divideInt
        , testQA "double" float Q.double A.double
        ]

ランダム値を再現する

CI でのテストにランダム値を組み込むと、「何もしていないのに気まぐれにテストが落ちる」というトラブルが起きて混乱しそうです。そこで、 seed を固定して毎回同じランダム値を生成することができます。

まず、何も考えずに elm-test を実行します。

$ elm-test

(略)

elm-test 0.18.9
---------------

Running 2 tests. To reproduce these results, run: elm-test --fuzz 100 --seed 1363676073

(略)

「この結果を再現するには elm-test --fuzz 100 --seed 1363676073 を実行してください」と書いてあるので、これをコピペして CI のスクリプトに貼り付けます。Travis CI だとおよそ次のようになるはずです。

language: node_js
node_js:
  - "6"
before_script:
  - npm install -g elm
  - npm install -g elm-test
  - elm-package install -y
script: elm-test --fuzz 100 --seed 1363676073

まとめ

テスト効率化のお供に、 Fuzzer の活用を是非ご検討ください!