affaan-m/ECC/docs/es/skills/kotlin-testing/SKILL.md
kotlin-testing
Patrones de pruebas Kotlin con Kotest, MockK, pruebas de coroutines, pruebas basadas en propiedades y cobertura con Kover. Sigue la metodología TDD con prácticas idiomáticas de Kotlin.
- Source repository stars
- 234,327
- Declared platforms
- 0
- Static risk flags
- 0
- Last source update
- 2026-07-27
- Source checked
- 2026-07-28
Decision brief
What it does—and where it fits
Patrones completos de pruebas Kotlin para escribir pruebas confiables y mantenibles siguiendo la metodología TDD con Kotest y MockK.
Not for
- Tasks that require unconfirmed production actions or broad system permissions.
- Environments where the pinned source and install steps cannot be inspected.
Compatibility matrix
Platform support, with evidence labels
| Platform | Status | Evidence | What to check |
|---|---|---|---|
| Codex | Not declared | No explicit evidence | Portability before use |
| Claude Code | Not declared | No explicit evidence | Portability before use |
| Cursor | Not declared | No explicit evidence | Portability before use |
| Gemini CLI | Not declared | No explicit evidence | Portability before use |
Installation
Inspect first. Install second.
The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.
npx skills add https://github.com/affaan-m/ECC --skill "docs/es/skills/kotlin-testing"Inspect the Agent Skill "kotlin-testing" from https://github.com/affaan-m/ECC/blob/4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38/docs/es/skills/kotlin-testing/SKILL.md at commit 4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.
Workflow
What the source asks the agent to do
- 01
Cuándo Usar
Escribir nuevas funciones o clases Kotlin
Escribir nuevas funciones o clases KotlinAgregar cobertura de pruebas a código Kotlin existenteImplementar pruebas basadas en propiedades - 02
Cómo Funciona
1. Identificar el código objetivo — Encontrar la función, clase o módulo a probar 2. Escribir un spec Kotest — Elegir un estilo de spec (StringSpec, FunSpec, BehaviorSpec) acorde al alcance de la prueba 3. Mockear dependencias — Usar MockK para aislar la unidad bajo prueba 4. Ej…
Identificar el código objetivo — Encontrar la función, clase o módulo a probarEscribir un spec Kotest — Elegir un estilo de spec (StringSpec, FunSpec, BehaviorSpec) acorde al alcance de la pruebaMockear dependencias — Usar MockK para aislar la unidad bajo prueba - 03
Ejemplos
Las siguientes secciones contienen ejemplos detallados y ejecutables para cada patrón de prueba:
Specs Kotest — Ejemplos de StringSpec, FunSpec, BehaviorSpec, DescribeSpec en Estilos de Spec KotestMocking — Configuración de MockK, mocking de coroutines, captura de argumentos en MockKFlujo de trabajo TDD — Ciclo RED/GREEN/REFACTOR completo con EmailValidator en Flujo de Trabajo TDD para Kotlin - 04
Referencia Rápida
Specs Kotest — Ejemplos de StringSpec, FunSpec, BehaviorSpec, DescribeSpec en Estilos de Spec Kotest
Specs Kotest — Ejemplos de StringSpec, FunSpec, BehaviorSpec, DescribeSpec en Estilos de Spec KotestMocking — Configuración de MockK, mocking de coroutines, captura de argumentos en MockKFlujo de trabajo TDD — Ciclo RED/GREEN/REFACTOR completo con EmailValidator en Flujo de Trabajo TDD para Kotlin - 05
Flujo de Trabajo TDD para Kotlin
Review the “Flujo de Trabajo TDD para Kotlin” section in the pinned source before continuing.
Review and apply the “Flujo de Trabajo TDD para Kotlin” source section.
Permission review
Static risk signals and limitations
No configured static risk pattern was detected
This is not proof of safety. Runtime behavior, indirect dependencies, and hidden external systems are outside the static scan.
Evidence record
Why each signal appears
| Signal | Value | Evidence type | Meaning |
|---|---|---|---|
| Quality score | 80/100 | Computed | Documentation, specificity, maintenance, and trust rules |
| Repository stars | 234,327 | Source | Repository attention, not individual Skill quality |
| Compatibility | 0 platforms | Source | Declared in the catalog source record |
| Usage guide | automated source guide | Editorial | Generated or reviewed according to the visible evidence level |
Pinned source
Provenance and original SKILL.md
- Repository
- affaan-m/ECC
- Skill path
- docs/es/skills/kotlin-testing/SKILL.md
- Commit
- 4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38
- License
- MIT
- Collected
- 2026-07-28
- Default branch
- main
View the original SKILL.md
Patrones de Pruebas Kotlin
Patrones completos de pruebas Kotlin para escribir pruebas confiables y mantenibles siguiendo la metodología TDD con Kotest y MockK.
Cuándo Usar
- Escribir nuevas funciones o clases Kotlin
- Agregar cobertura de pruebas a código Kotlin existente
- Implementar pruebas basadas en propiedades
- Seguir el flujo de trabajo TDD en proyectos Kotlin
- Configurar Kover para cobertura de código
Cómo Funciona
- Identificar el código objetivo — Encontrar la función, clase o módulo a probar
- Escribir un spec Kotest — Elegir un estilo de spec (StringSpec, FunSpec, BehaviorSpec) acorde al alcance de la prueba
- Mockear dependencias — Usar MockK para aislar la unidad bajo prueba
- Ejecutar pruebas (ROJO) — Verificar que la prueba falla con el error esperado
- Implementar código (VERDE) — Escribir el código mínimo para pasar la prueba
- Refactorizar — Mejorar la implementación manteniendo las pruebas en verde
- Verificar cobertura — Ejecutar
./gradlew koverHtmlReporty verificar 80%+ de cobertura
Ejemplos
Las siguientes secciones contienen ejemplos detallados y ejecutables para cada patrón de prueba:
Referencia Rápida
- Specs Kotest — Ejemplos de StringSpec, FunSpec, BehaviorSpec, DescribeSpec en Estilos de Spec Kotest
- Mocking — Configuración de MockK, mocking de coroutines, captura de argumentos en MockK
- Flujo de trabajo TDD — Ciclo RED/GREEN/REFACTOR completo con EmailValidator en Flujo de Trabajo TDD para Kotlin
- Cobertura — Configuración de Kover y comandos en Cobertura con Kover
- Pruebas Ktor — Configuración de testApplication en Pruebas con Ktor testApplication
Flujo de Trabajo TDD para Kotlin
El Ciclo ROJO-VERDE-REFACTORIZAR
ROJO -> Escribir primero una prueba fallida
VERDE -> Escribir el código mínimo para pasar la prueba
REFACTORIZAR -> Mejorar el código manteniendo las pruebas en verde
REPETIR -> Continuar con el siguiente requisito
TDD Paso a Paso en Kotlin
// Paso 1: Definir la interfaz/firma
// EmailValidator.kt
package com.example.validator
fun validateEmail(email: String): Result<String> {
TODO("not implemented")
}
// Paso 2: Escribir la prueba fallida (ROJO)
// EmailValidatorTest.kt
package com.example.validator
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.result.shouldBeFailure
import io.kotest.matchers.result.shouldBeSuccess
class EmailValidatorTest : StringSpec({
"valid email returns success" {
validateEmail("user@example.com").shouldBeSuccess("user@example.com")
}
"empty email returns failure" {
validateEmail("").shouldBeFailure()
}
"email without @ returns failure" {
validateEmail("userexample.com").shouldBeFailure()
}
})
// Paso 3: Ejecutar pruebas - verificar FALLO
// $ ./gradlew test
// EmailValidatorTest > valid email returns success FAILED
// kotlin.NotImplementedError: An operation is not implemented
// Paso 4: Implementar el código mínimo (VERDE)
fun validateEmail(email: String): Result<String> {
if (email.isBlank()) return Result.failure(IllegalArgumentException("Email cannot be blank"))
if ('@' !in email) return Result.failure(IllegalArgumentException("Email must contain @"))
val regex = Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
if (!regex.matches(email)) return Result.failure(IllegalArgumentException("Invalid email format"))
return Result.success(email)
}
// Paso 5: Ejecutar pruebas - verificar PASE
// $ ./gradlew test
// EmailValidatorTest > valid email returns success PASSED
// EmailValidatorTest > empty email returns failure PASSED
// EmailValidatorTest > email without @ returns failure PASSED
// Paso 6: Refactorizar si es necesario, verificar que las pruebas siguen pasando
Estilos de Spec Kotest
StringSpec (El Más Simple)
class CalculatorTest : StringSpec({
"add two positive numbers" {
Calculator.add(2, 3) shouldBe 5
}
"add negative numbers" {
Calculator.add(-1, -2) shouldBe -3
}
"add zero" {
Calculator.add(0, 5) shouldBe 5
}
})
FunSpec (Similar a JUnit)
class UserServiceTest : FunSpec({
val repository = mockk<UserRepository>()
val service = UserService(repository)
test("getUser returns user when found") {
val expected = User(id = "1", name = "Alice")
coEvery { repository.findById("1") } returns expected
val result = service.getUser("1")
result shouldBe expected
}
test("getUser throws when not found") {
coEvery { repository.findById("999") } returns null
shouldThrow<UserNotFoundException> {
service.getUser("999")
}
}
})
BehaviorSpec (Estilo BDD)
class OrderServiceTest : BehaviorSpec({
val repository = mockk<OrderRepository>()
val paymentService = mockk<PaymentService>()
val service = OrderService(repository, paymentService)
Given("a valid order request") {
val request = CreateOrderRequest(
userId = "user-1",
items = listOf(OrderItem("product-1", quantity = 2)),
)
When("the order is placed") {
coEvery { paymentService.charge(any()) } returns PaymentResult.Success
coEvery { repository.save(any()) } answers { firstArg() }
val result = service.placeOrder(request)
Then("it should return a confirmed order") {
result.status shouldBe OrderStatus.CONFIRMED
}
Then("it should charge payment") {
coVerify(exactly = 1) { paymentService.charge(any()) }
}
}
When("payment fails") {
coEvery { paymentService.charge(any()) } returns PaymentResult.Declined
Then("it should throw PaymentException") {
shouldThrow<PaymentException> {
service.placeOrder(request)
}
}
}
}
})
DescribeSpec (Estilo RSpec)
class UserValidatorTest : DescribeSpec({
describe("validateUser") {
val validator = UserValidator()
context("with valid input") {
it("accepts a normal user") {
val user = CreateUserRequest("Alice", "alice@example.com")
validator.validate(user).shouldBeValid()
}
}
context("with invalid name") {
it("rejects blank name") {
val user = CreateUserRequest("", "alice@example.com")
validator.validate(user).shouldBeInvalid()
}
it("rejects name exceeding max length") {
val user = CreateUserRequest("A".repeat(256), "alice@example.com")
validator.validate(user).shouldBeInvalid()
}
}
}
})
Matchers de Kotest
Matchers Principales
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.string.*
import io.kotest.matchers.collections.*
import io.kotest.matchers.nulls.*
// Igualdad
result shouldBe expected
result shouldNotBe unexpected
// Strings
name shouldStartWith "Al"
name shouldEndWith "ice"
name shouldContain "lic"
name shouldMatch Regex("[A-Z][a-z]+")
name.shouldBeBlank()
// Colecciones
list shouldContain "item"
list shouldHaveSize 3
list.shouldBeSorted()
list.shouldContainAll("a", "b", "c")
list.shouldBeEmpty()
// Nulls
result.shouldNotBeNull()
result.shouldBeNull()
// Tipos
result.shouldBeInstanceOf<User>()
// Números
count shouldBeGreaterThan 0
price shouldBeInRange 1.0..100.0
// Excepciones
shouldThrow<IllegalArgumentException> {
validateAge(-1)
}.message shouldBe "Age must be positive"
shouldNotThrow<Exception> {
validateAge(25)
}
Matchers Personalizados
fun beActiveUser() = object : Matcher<User> {
override fun test(value: User) = MatcherResult(
value.isActive && value.lastLogin != null,
{ "User ${value.id} should be active with a last login" },
{ "User ${value.id} should not be active" },
)
}
// Uso
user should beActiveUser()
MockK
Mocking Básico
class UserServiceTest : FunSpec({
val repository = mockk<UserRepository>()
val logger = mockk<Logger>(relaxed = true) // Relaxed: retorna valores por defecto
val service = UserService(repository, logger)
beforeTest {
clearMocks(repository, logger)
}
test("findUser delegates to repository") {
val expected = User(id = "1", name = "Alice")
every { repository.findById("1") } returns expected
val result = service.findUser("1")
result shouldBe expected
verify(exactly = 1) { repository.findById("1") }
}
test("findUser returns null for unknown id") {
every { repository.findById(any()) } returns null
val result = service.findUser("unknown")
result.shouldBeNull()
}
})
Mocking de Coroutines
class AsyncUserServiceTest : FunSpec({
val repository = mockk<UserRepository>()
val service = UserService(repository)
test("getUser suspending function") {
coEvery { repository.findById("1") } returns User(id = "1", name = "Alice")
val result = service.getUser("1")
result.name shouldBe "Alice"
coVerify { repository.findById("1") }
}
test("getUser with delay") {
coEvery { repository.findById("1") } coAnswers {
delay(100) // Simular trabajo asíncrono
User(id = "1", name = "Alice")
}
val result = service.getUser("1")
result.name shouldBe "Alice"
}
})
Captura de Argumentos
test("save captures the user argument") {
val slot = slot<User>()
coEvery { repository.save(capture(slot)) } returns Unit
service.createUser(CreateUserRequest("Alice", "alice@example.com"))
slot.captured.name shouldBe "Alice"
slot.captured.email shouldBe "alice@example.com"
slot.captured.id.shouldNotBeNull()
}
Spy y Mocking Parcial
test("spy on real object") {
val realService = UserService(repository)
val spy = spyk(realService)
every { spy.generateId() } returns "fixed-id"
spy.createUser(request)
verify { spy.generateId() } // Sobreescrito
// Otros métodos usan la implementación real
}
Pruebas de Coroutines
runTest para Funciones Suspend
import kotlinx.coroutines.test.runTest
class CoroutineServiceTest : FunSpec({
test("concurrent fetches complete together") {
runTest {
val service = DataService(testScope = this)
val result = service.fetchAllData()
result.users.shouldNotBeEmpty()
result.products.shouldNotBeEmpty()
}
}
test("timeout after delay") {
runTest {
val service = SlowService()
shouldThrow<TimeoutCancellationException> {
withTimeout(100) {
service.slowOperation() // Tarda > 100ms
}
}
}
}
})
Pruebas de Flows
import io.kotest.matchers.collections.shouldContainInOrder
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
class FlowServiceTest : FunSpec({
test("observeUsers emits updates") {
runTest {
val service = UserFlowService()
val emissions = service.observeUsers()
.take(3)
.toList()
emissions shouldHaveSize 3
emissions.last().shouldNotBeEmpty()
}
}
test("searchUsers debounces input") {
runTest {
val service = SearchService()
val queries = MutableSharedFlow<String>()
val results = mutableListOf<List<User>>()
val job = launch {
service.searchUsers(queries).collect { results.add(it) }
}
queries.emit("a")
queries.emit("ab")
queries.emit("abc") // Solo este debería disparar la búsqueda
advanceTimeBy(500)
results shouldHaveSize 1
job.cancel()
}
}
})
TestDispatcher
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
class DispatcherTest : FunSpec({
test("uses test dispatcher for controlled execution") {
val dispatcher = StandardTestDispatcher()
runTest(dispatcher) {
var completed = false
launch {
delay(1000)
completed = true
}
completed shouldBe false
advanceTimeBy(1000)
completed shouldBe true
}
}
})
Pruebas Basadas en Propiedades
Pruebas de Propiedades con Kotest
import io.kotest.core.spec.style.FunSpec
import io.kotest.property.Arb
import io.kotest.property.arbitrary.*
import io.kotest.property.forAll
import io.kotest.property.checkAll
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString
import kotlinx.serialization.decodeFromString
// Nota: La prueba de roundtrip de serialización requiere que la data class User
// esté anotada con @Serializable (de kotlinx.serialization).
class PropertyTest : FunSpec({
test("string reverse is involutory") {
forAll<String> { s ->
s.reversed().reversed() == s
}
}
test("list sort is idempotent") {
forAll(Arb.list(Arb.int())) { list ->
list.sorted() == list.sorted().sorted()
}
}
test("serialization roundtrip preserves data") {
checkAll(Arb.bind(Arb.string(1..50), Arb.string(5..100)) { name, email ->
User(name = name, email = "$email@test.com")
}) { user ->
val json = Json.encodeToString(user)
val decoded = Json.decodeFromString<User>(json)
decoded shouldBe user
}
}
})
Generadores Personalizados
val userArb: Arb<User> = Arb.bind(
Arb.string(minSize = 1, maxSize = 50),
Arb.email(),
Arb.enum<Role>(),
) { name, email, role ->
User(
id = UserId(UUID.randomUUID().toString()),
name = name,
email = Email(email),
role = role,
)
}
val moneyArb: Arb<Money> = Arb.bind(
Arb.long(1L..1_000_000L),
Arb.enum<Currency>(),
) { amount, currency ->
Money(amount, currency)
}
Pruebas Dirigidas por Datos
withData en Kotest
class ParserTest : FunSpec({
context("parsing valid dates") {
withData(
"2026-01-15" to LocalDate(2026, 1, 15),
"2026-12-31" to LocalDate(2026, 12, 31),
"2000-01-01" to LocalDate(2000, 1, 1),
) { (input, expected) ->
parseDate(input) shouldBe expected
}
}
context("rejecting invalid dates") {
withData(
nameFn = { "rejects '$it'" },
"not-a-date",
"2026-13-01",
"2026-00-15",
"",
) { input ->
shouldThrow<DateParseException> {
parseDate(input)
}
}
}
})
Ciclo de Vida y Fixtures de Prueba
BeforeTest / AfterTest
class DatabaseTest : FunSpec({
lateinit var db: Database
beforeSpec {
db = Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1")
transaction(db) {
SchemaUtils.create(UsersTable)
}
}
afterSpec {
transaction(db) {
SchemaUtils.drop(UsersTable)
}
}
beforeTest {
transaction(db) {
UsersTable.deleteAll()
}
}
test("insert and retrieve user") {
transaction(db) {
UsersTable.insert {
it[name] = "Alice"
it[email] = "alice@example.com"
}
}
val users = transaction(db) {
UsersTable.selectAll().map { it[UsersTable.name] }
}
users shouldContain "Alice"
}
})
Extensiones de Kotest
// Extensión de prueba reutilizable
class DatabaseExtension : BeforeSpecListener, AfterSpecListener {
lateinit var db: Database
override suspend fun beforeSpec(spec: Spec) {
db = Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1")
}
override suspend fun afterSpec(spec: Spec) {
// limpieza
}
}
class UserRepositoryTest : FunSpec({
val dbExt = DatabaseExtension()
register(dbExt)
test("save and find user") {
val repo = UserRepository(dbExt.db)
// ...
}
})
Cobertura con Kover
Configuración de Gradle
// build.gradle.kts
plugins {
id("org.jetbrains.kotlinx.kover") version "0.9.7"
}
kover {
reports {
total {
html { onCheck = true }
xml { onCheck = true }
}
filters {
excludes {
classes("*.generated.*", "*.config.*")
}
}
verify {
rule {
minBound(80) // Fallar el build por debajo del 80% de cobertura
}
}
}
}
Comandos de Cobertura
# Ejecutar pruebas con cobertura
./gradlew koverHtmlReport
# Verificar umbrales de cobertura
./gradlew koverVerify
# Reporte XML para CI
./gradlew koverXmlReport
# Ver reporte HTML (usa el comando para tu SO)
# macOS: open build/reports/kover/html/index.html
# Linux: xdg-open build/reports/kover/html/index.html
# Windows: start build/reports/kover/html/index.html
Objetivos de Cobertura
| Tipo de Código | Objetivo |
|---|---|
| Lógica de negocio crítica | 100% |
| APIs públicas | 90%+ |
| Código general | 80%+ |
| Código generado / configuración | Excluir |
Pruebas con Ktor testApplication
class ApiRoutesTest : FunSpec({
test("GET /users returns list") {
testApplication {
application {
configureRouting()
configureSerialization()
}
val response = client.get("/users")
response.status shouldBe HttpStatusCode.OK
val users = response.body<List<UserResponse>>()
users.shouldNotBeEmpty()
}
}
test("POST /users creates user") {
testApplication {
application {
configureRouting()
configureSerialization()
}
val response = client.post("/users") {
contentType(ContentType.Application.Json)
setBody(CreateUserRequest("Alice", "alice@example.com"))
}
response.status shouldBe HttpStatusCode.Created
}
}
})
Comandos de Prueba
# Ejecutar todas las pruebas
./gradlew test
# Ejecutar clase de prueba específica
./gradlew test --tests "com.example.UserServiceTest"
# Ejecutar prueba específica
./gradlew test --tests "com.example.UserServiceTest.getUser returns user when found"
# Ejecutar con salida detallada
./gradlew test --info
# Ejecutar con cobertura
./gradlew koverHtmlReport
# Ejecutar detekt (análisis estático)
./gradlew detekt
# Ejecutar ktlint (verificación de formato)
./gradlew ktlintCheck
# Pruebas continuas
./gradlew test --continuous
Buenas Prácticas
HACER:
- Escribir pruebas PRIMERO (TDD)
- Usar los estilos de spec de Kotest de forma consistente en el proyecto
- Usar
coEvery/coVerifyde MockK para funciones suspend - Usar
runTestpara pruebas de coroutines - Probar comportamiento, no implementación
- Usar pruebas basadas en propiedades para funciones puras
- Usar fixtures de
data classpara mayor claridad
NO HACER:
- Mezclar frameworks de prueba (elegir Kotest y mantenerlo)
- Mockear data classes (usar instancias reales)
- Usar
Thread.sleep()en pruebas de coroutines (usaradvanceTimeBy) - Saltarse la fase ROJA en TDD
- Probar funciones privadas directamente
- Ignorar pruebas inestables (flaky tests)
Integración con CI/CD
# Ejemplo de GitHub Actions
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
- name: Run tests with coverage
run: ./gradlew test koverXmlReport
- name: Verify coverage
run: ./gradlew koverVerify
- name: Upload coverage
uses: codecov/codecov-action@v5
with:
files: build/reports/kover/report.xml
token: ${{ secrets.CODECOV_TOKEN }}
Recuerda: Las pruebas son documentación. Muestran cómo debe usarse tu código Kotlin. Usa los matchers expresivos de Kotest para que las pruebas sean legibles y MockK para un mocking limpio de dependencias.
Alternatives
Compare before choosing
affaan-m/ECC
kotlin-testing
Kotlin testing patterns with Kotest, MockK, coroutine testing, property-based testing, and Kover coverage. Follows TDD methodology with idiomatic Kotlin practices.
affaan-m/ECC
kotlin-testing
Use it for testing and engineering tasks; the detail page covers purpose, installation, and practical steps.
affaan-m/ECC
kotlin-testing
Kotest, MockK, coroutine testi, property-based testing ve Kover coverage ile Kotlin test kalıpları. İdiomatic Kotlin uygulamalarıyla TDD metodolojisini takip eder.
coreyhaines31/marketingskills
ab-testing
When the user wants to plan, design, or implement an A/B test or experiment, or build a growth experimentation program. Also use when the user mentions "A/B test," "split test," "experiment," "test this change," "variant copy," "multivariate test," "hypothesis," "should I test this," "which version is better," "test two versions," "statistical significance," "how long should I run this test," "growth experiments," "experiment velocity," "experiment backlog," "ICE score," "experimentation program