この記事はKotlin Advent Calendar 2024 15日目の記事です。
2024/11/26 Kotlin愛好会 にて お話ししたCucumber を記事にします。
Cucumber
- 振舞駆動開発; behavior driven development, BDD をサポートするツール
- Featureファイルに記述した自然言語のテストケース テスト関数を生成してくれるような仕組み
- サポート言語は多岐にわたる
Featureファイル
例えば
Feature: Belly
Scenario: a few cukes
Given I have 42 cukes in my belly
When I wait 1 hour
Then my belly should growl
Gherkin記法でテストケースを記述するファイル用意して, BDD-jvm に食わせると以下が吐き出されるので それにテストコードを書いていく形
Given("I have {int} cukes in my belly", (Integer int1) -> {
// Write code here that turns the phrase above into concrete actions
throw new io.cucumber.java8.PendingException();
});
When("I wait {int} hour", (Integer int1) -> {
// Write code here that turns the phrase above into concrete actions
throw new io.cucumber.java8.PendingException();
});
Then("my belly should growl", () -> {
// Write code here that turns the phrase above into concrete actions
throw new io.cucumber.java8.PendingException();
});
Gherkin記法
この記法自体も Cucumberのコミュニティが メンテナンスしてる
BDD に沿った記述のしやすい記法
Feature: Guess the word
# The first example has two steps
Scenario: Maker starts a game
When the Maker starts a game
Then the Maker waits for a Breaker to join
# The second example has three steps
Scenario: Breaker joins a game
Given the Maker has started a game with the word "silky"
When the Breaker joins the Maker's game
Then the Breaker must guess a word with 5 characters
ざっくり以下
Feature: 機能名の記述
Scenario: テストシナリオの記述、ビジネスルールの記述 etc
Given テストの前提条件
When アクションやイベントを記述
Then 期待される結果 「~するべき」 の部分
全ては紹介しきれないが 他にも色々👇
Feature
-
Rule
(as of Gherkin 6) -
Example
(orScenario
) -
Given
,When
,Then
,And
,But
for steps (or ``) Background
-
Scenario Outline
(orScenario Template
) -
Examples
(orScenarios
)
例えば
Android では Instrumentation Test をサポートしている
認証したらアプリのサービスを確認できる みたいなシナリオを書いてみると
Feature: ログイン
ユーザストーリー: ログインするとホームを確認できる # descriptionが書ける
Scenario: ログインするとホームを確認できる
Given ログインID "anpanman" パスワード "password"
When ログインした
Then ホーム画面が確認できる
Scenario: ログインに失敗する
Given ログインID "aaaaaaa" パスワード "passward"
When ログインしようとする
Then ホームが表示されない
@HiltAndroidTest
class LoginScenario @Inject constructor(
private val composeRuleHolder: ComposeRuleHolder
): SemanticsNodeInteractionsProvider by composeRuleHolder.activityComposeRule {
private val rule = composeRuleHolder.activityComposeRule
@Given("ログインID {string} パスワード {string}")
fun ログインid_パスワード(string: String, string2: String) {
rule.waitUntilAtLeastOneExists(
hasTestTag("login-screen-content"),
3000
)
onNodeWithTag("id-textfield")
.performTextInput(string)
onNodeWithTag("pass-textfield")
.performTextInput(string2)
}
@When("ログインした")
fun ログインした( ) {
onNodeWithTag("login-button")
.performClick()
}
@Then("ホーム画面が確認できる")
fun ホーム画面が確認できる( ) {
rule.waitUntilAtLeastOneExists(
hasTestTag("home"),
5000
)
onNodeWithTag("home").assertExists()
}
@When("ログインしようとする")
fun ログインしようとする( ) {
onNodeWithTag("login-button")
.performClick()
}
@Then("ホームが表示されない")
fun ホームが表示されない( ) {
rule.waitUntilAtLeastOneExists(
hasTestTag("login-circular"),
5000
)
rule.waitUntilDoesNotExist(
hasTestTag("login-circular"),
5000
)
onNodeWithTag("home").assertDoesNotExist()
}
}
セットアップは少々Stepあり
Cucumberは テストスイートを CucumberOptions
で定義して Cucumberがそれを見つけて実行するような仕組み
composeRule や ActivityTestRule はそのままだとテストファイルに宣言しても使えないため RuleHolder的なコンテナをHiltで注入する仕組みになっている。
implementation(libs.hilt.android) // androidTestImplementationのみでは警告
ksp(libs.hilt.compiler)
kspAndroidTest(libs.hilt.compiler)
androidTestImplementation(libs.hilt.testing)
androidTestImplementation(libs.cucumber.android)
androidTestImplementation(libs.cucumber.android.hilt)
エントリとなるテストスイートを 専用の CucumberOption
アノテーションで作成する。Hiltのテストのセットアップもここで行う。
@CucumberOptions(
features = [ "features" ],
glue = ["com.example.examplecucumberandroid"]
)
class MyAndroidCucumberTestRunner : CucumberAndroidJUnitRunner() {
override fun newApplication(
cl: ClassLoader?,
className: String?,
context: Context?
): Application {
return super.newApplication(
cl,
HiltTestApplication::class.java.name,
context
)
}
}
前述の コンテナを準備 @WithJunitRule
で org.junit.Rule
を使用するためクラスを作る
@WithJunitRule
@Singleton
class ComposeRuleHolder @Inject constructor() {
@get:Rule(order = 1)
val composeRule = createComposeRule()
@get:Rule(order = 1)
val activityComposeRule = createAndroidComposeRule<MainActivity>()
}
あとは 前述通り
@HiltAndroidTest
class LoginScenario @Inject constructor(
private val composeRuleHolder: ComposeRuleHolder
): SemanticsNodeInteractionsProvider by composeRuleHolder.activityComposeRule {
private val rule = composeRuleHolder.activityComposeRule
@Given("ログインID {string} パスワード {string}")
fun ログインid_パスワード(string: String, string2: String) {...
うまみはあるか?
以下目的ではいいのかもしれない
-
仕様側と 開発側が そこそこ明確に分かれて、仕様と実装の間に 共通言語となるドキュメントが欲しい
-
iOS , Android みたいな 各プラットフォームで共通のテスト仕様 を 書く
-
BDD , TDD 自体を推進したい
- BDD で決めた振る舞い ベースのテスト仕様から TDD に繋げて プロダクションコードを設計する みたいな手法があるが それの一部で使うみたいなイメージ