はじめに
この記事は2020年のRevCommアドベントカレンダー8日目の記事です。7日目は@seckieさんの「Adobe XD を使った Web 開発のワークフロー」でした。
こんにちは。RevCommで主にAndroid開発を担当している @marienplatz です。
テスト導入の経験がなかったので「やってみたい!」と立候補したところ、せっかくなので記事にしちゃえとお題が決まりました。
目次
-
- 最もシンプルなユニットテストを導入
- 1.1. テストコードを書く準備
- 1.2. テストを導入
-
- 複雑なメソッドのテストテクニック(スタブを使用)
- 2.1. 問題のあるメソッドの作成
- 2.2. スタブの使用
超入門的な内容から徐々に本格的な内容を!という予定でしたが、時間切れで入門のみで終えてしまい目次を見返しては溜め息をついています。。。
それはさておき早速ですが、テストコードを導入していきます。
1. 最もシンプルなテストを導入
1.1. テストコードを書く準備
四則計算をするだけのプロジェクトを作成しました。
左のeditTextと右のeditTextに数字を、真ん中のeditTextに四則演算子を記入し、
計算ボタンを押したら計算結果を表示するというものです。
ユーザーが手入力するので要らない空白が入ってしまったり、半角を想定しているのに全角数字がきてしまったり何でもアリ!なのでvalidatorは欠かせません。
class Validator {
fun isValidNumber(text: String): Boolean {
return text.matches(Regex("[0-9]+"))
}
}
というわけで早速validatorを書きました。
このvalidatorクラスのテストコードを書いていきます。
まずは、下の画像の「Validator」という文字列にカーソルを持ってきて、
command + shift + t を押します。
Create New Test...をクリックすると、
こんなダイアログが表示されるので、
tearDown/@After
をチェックしてOKを押してください。
そうするとテスト用のディレクトリにファイルが自動生成されます。
class ValidatorTest : TestCase() {
public override fun setUp() {
super.setUp()
}
public override fun tearDown() {}
}
こんなファイルが自動生成されました。
Validatorクラスに対応したクラスです。
先ほどチェックを入れたおかげで自動生成されたsetUp()やtearDown()というメソッドですが、
setUp()は初期化、tearDown()は後処理に使います。
これで下準備は完了です!
1.2. テストコードを導入
いよいよテストコードを導入します!
@RunWith(JUnit4::class)
class ValidatorTest : TestCase() {
public override fun setUp() {
super.setUp()
}
public override fun tearDown() {}
@Test
fun isValidNumber_givenAlpha_returnsFalse() {
val target = Validator()
val actual = target.isValidNumber("a")
assertThat(actual, `is`(false))
}
}
アルファベットが与えられたときはfalseを返す、というテストが書けました!
このテストを実行するにはandroid studioのテストメソッドの右側にある緑の実行ボタン(下の画像参照)を押すと実行されます。
assertThat()でisValidNumber()の返り値がfalseであることを確認しています。
もしここでtrueが返ってきたとき(テストに失敗したとき)はAssertionFailedErrorという例外が投げられます。
無事にテスト導入ができました!
これくらいシンプルなユニットテストであれば少し手順を覚えるだけなので簡単ですよね。
が、実務で書かれているメソッドであればこんな風にシンプルにテストできるものばかりではないはずです。
次は複雑なメソッドのテスト方法について扱っていきたいと思います。
2. 複雑なメソッドのテストテクニック(スタブを使用)
2.1. 問題のあるメソッドの作成
前章でvalidator()を作成しました。
今度はそれらを用いてユーザーの入力内容は計算可能かチェックし、不可能であればエラーメッセージを取得するメソッドを作ってみます。
open class Validator {
open fun isValidNumber(text: String): Boolean {
return text.matches(Regex("[0-9]+"))
}
// 演算子のチェックも一応追加しました
open fun isValidOperator(text: String): Boolean {
return text.matches(Regex("""[-+*/]"""))
}
}
data class InputNumbers(
val num1: String,
val num2: String,
val operator: String
)
open class InputChecker {
var errMsg: String = ""
open fun canCalc(input: InputNumbers): Boolean {
val validator = Validator()
if (!validator.isValidNumber(input.num1)) {
errMsg = "数字を入力してください"
return false
}
if (!validator.isValidNumber(input.num2)) {
errMsg = "数字を入力してください"
return false
}
if (!validator.isValidOperator(input.operator)) {
errMsg = "+, -, *, /のいずれかを入力してください"
return false
}
// クラッシュが気になって追加しましたがここでは重要でないです
if (input.operator == "/" && input.num2 == "0") {
errMsg = "0で割らないでください"
return false
}
return true
}
}
このcanCalc()というメソッドなのですが、テストをしようにも、前章で作ったvalidatorクラスに依存してしまっています。(ここが問題です。)
バグの切り分けがしづらいですし、運が悪いとバグが打ち消しあってしまって気付けないなんてことも起こりえます。
この依存を取り除く手段の一つが、スタブを用いることです。
2.2. スタブの使用
open class Validator {
open fun isValidNumber(text: String): Boolean {
return text.matches(Regex("[0-9]+"))
}
open fun isValidOperator(text: String): Boolean {
return text.matches(Regex("""[-+*/]"""))
}
}
class StubValidator(private val isNumber: Boolean, private val isOperator: Boolean): Validator() {
override fun isValidNumber(text: String): Boolean {
return isNumber
}
override fun isValidOperator(text: String): Boolean {
return isOperator
}
}
StubValidatorクラスを追加しました。(Validatorクラスを継承しています)
ValidatorクラスをStubValidatorクラスに置き換えて、コンストラクタにisValidNumber()やisValidOperator()に返して欲しい真偽値を持たせることにより、狙った返り値が得られます。
例えば、「isValidNumber()がfalseを返して、isValidOperator()はtrueを返すテストをしたいな」と思ったときはValidator()をStubValidator(false, true)に置き換えることで代替可能です。
「ところで、ValidatorクラスとStubValidatorクラスを差し替えるってどうやるの?」という話になるわけですが、
InputCheckerクラスのcanCalc()の引数にValidatorクラスを追加します。
これによってStubValidatorクラスと差し替え可能になります。
BeforeではValidatorクラスをcanCalc()内で初期化していました。
open class InputChecker {
open fun canCalc(input: Operator.InputNumbers): Boolean {
val validator = Validator()
(略)
}
引数に差し替えの必要なValidatorクラスを置きますと
open class InputChecker {
open fun canCalc(input: Operator.InputNumbers, validator: Validator): Boolean {
(略)
}
}
テスト時に引数をStubValidatorクラスに差し替えることにより容易にテストできるようになります。(より詳しく知りたい方はDI(依存性注入)について調べてみると良いかもです)
スタブを用いたテストコード例です。
@RunWith(JUnit4::class)
class InputCheckerTest {
lateinit var target: InputChecker
@Before
fun setUp() {
target = InputChecker()
}
@After
fun tearDown() {
}
// 入力された値が全て正常なとき
@Test
fun checkCanCalc_givenValidValues_returnsTrue() {
// inputの値はなんでもいい
val input = InputNumbers("11", "100", "+")
val stubValidator = StubValidator(isNumber = true, isOperator = true)
assertThat(target.canCalc(input, stubValidator), `is`(true))
}
// 入力された演算子が異常なとき
@Test
fun checkCanCalc_givenInvalidOperator_returnsFalse() {
// inputの値はなんでもいい
val input = InputNumbers("11", "100", "+")
val stubValidator = StubValidator(isNumber = true, isOperator = false)
assertThat(target.canCalc(input, stubValidator), `is`(false))
assertThat(target.errMsg == "+, -, *, /のいずれかを入力してください", `is`(true))
}
// 入力された数字が異常なとき
@Test
fun checkCanCalc_givenInvalidNumber_returnsFalse() {
// inputの値はなんでもいい
val input = InputNumbers("11", "100", "+")
val stubValidator = StubValidator(isNumber = false, isOperator = true)
assertThat(target.canCalc(input, stubValidator), `is`(false))
assertThat(target.errMsg == "数字を入力してください", `is`(true))
}
}
このように、スタブを用いることによりvalidatorクラスのメソッドの返り値が真であった場合と偽であった場合のそれぞれのケースで容易にテストすることができました。
スタブのおかげでvalidatorクラスに依存することなくユニットテストが可能となりました。
以上が複雑なメソッドのテストテクニックについてです。
おわりに
本当はUIテストまで書きたかったのですがとても収拾がつかなさそうなのでここで終わります。
長くなりましたがお付き合いいただき、ありがとうございました。
明日は @sukekd さんの「Google Workspace(旧称G Suite) アカウントのSAML認証を使ってAWS CLIの認証を行う」です。
参考
- https://qiita.com/t12u/items/8c28484100dfd3a6351b
- Androidテスト全書(白山文彦、外山純生、平田敏之、菊池紘 著 2018-09-20 版 PEAKS(ピークス) 発行)