Sytuacja kobiet w IT w 2024 roku
13.08.20204 min
Catalin Gheorghe

Catalin GheorgheAndroid EngineerCognizant Softvision

Parametryzowane testy jednostkowe w Kotlinie

Sprawdź, jak pozbyć się powtarzającego się kodu, poprzez napisanie parametryzowanych testów z użyciem JUnit 5 w Kotlinie.

Parametryzowane testy jednostkowe w Kotlinie

Faktem jest, że każdy projekt skorzysta na dobrze napisanych testach jednostkowych. Są jednak pewne przypadki, gdzie pisanie testów jednostkowych jest powtarzalne i skutkuje powstawaniem zduplikowanego kodu, gdzie zmienia się tylko wejście i spodziewane wyjście.

Żeby pokazać taki przypadek, stwórzmy klasę, której zadaniem jest sprawdzenie, czy hasło spełnia określone kryteria. Poniżej mamy przykład takiej klasy, która ma metodę przyjmującą hasło do sprawdzenia i zwracającą true, jeżeli hasło spełnia wymagania. Hasło powinno mieć przynajmniej jedną wielką literę, jedną cyfrę, znak specjalny i 8 znaków.

class PasswordValidator {

    fun isValid(password: String) = PATTERN.matcher(password).matches()

    private companion object {
        val PATTERN: Pattern = Pattern.compile("^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@\$%^&*-]).{8,}\$")
    }
}


Pisanie testów jednostkowych dla tej klasy może skutkować masą kodu, napisanego wg schmatu “Given a password, When validating it, Then it is reported as valid/invalid”. Możemy z pewnością wymyślić tu coś lepszego - z pomocą przyjdzie nam JUnit 5.

Parametryzowane testy

Parametryzowane testy zostały dodane w JUnit 4, ale w tym artykule będziemy używać JUnit 5. Dzięki tej funkcji możemy stworzyć test i uruchomić go z różnymi parametrami. Jedyną rzeczą, jakiej potrzebujemy do utworzenia sparmetryzowanego testu jest adnotacja @ParametrizedTest i zapewnienia mu źródła danych. JUnit 5 pozwala na kilka sposobów przekazywania argumentów, mogą być one pobrane z metod, enumów, kolekcji i nawet CSV. Więcej przykładów i szczegółów znajdziesz w oficjalnej dokumentacji. Oto, jak może wyglądać sparametryzowany test dla naszej metody:

internal class PasswordValidatorTest {

    private val validator = PasswordValidator()

    @ParameterizedTest(name = "given \"{0}\", when validating the password, then it should return {1}")
    @MethodSource("passwordArguments")
    fun `given input password, when validating it, then is should return if it is valid`(
        password: String,
        expected: Boolean
    ) {
        val actual = validator.isValid(password)

        assertThat(actual).isEqualTo(expected)
    }

    private companion object {
        @JvmStatic
        fun passwordArguments() = Stream.of(
            Arguments.of("Test123!", true),
            Arguments.of("#tesT12!", true),
            Arguments.of("12Es@t123", true),
            Arguments.of("test123!", false),
            Arguments.of("t ", false),
            Arguments.of("   ", false)
        )
    }
}


Jeżeli używasz tego podejścia w projekcie androidowym, upewnij się, że Kotlin jest skonfigurowany, by kompilował się do Javy 8, dzięki czemu można użyć Stream i Arguments. Używanie Arguments pomaga przekazywaniu parametrów bez tworzenia dodatkowej klasy, która będzie je opakowywać.

Najpierw dodajemy adnotację @ParameterizedTest do testu i wpisujemy nazwę, której chcemy używać dla naszego testu. Możemy użyć {n}, by dobrać się do n-tego parametru. W tym przykładzie pobieramy argumenty z metody, która musi być statyczna. Ponieważ używamy Kotlina, musimy umieścić ją w companion object.

Jedną z przewag tego podejścia, poza zredukowaniem boilerplate’u, jest to, że dla każdego argumentu funkcja zachowuje się jak normalny test, więc metody Before i After są wywoływane dla każdego z nich. Bardzo się to przyda do inicjalizacji i czyszczenia (czego nie potrzebujemy w naszym przykładzie). Nie udało mi się znaleźć sposobu na dostosowanie nazwy testu, poza dodaniem do niej argumentów, więc dla spodziewanego rezultatu dostawałem słowa true/false.

Dynamiczne testy

JUnit 5 dodał nowe podejście do pisania testów jednostkowych. Tradycyjnie testy są statyczne i w pełni zdefiniowane w czasie kompilacji. Dynamiczne testy są generowane w czasie wykonania poprzez użycie adnotacji @TestFactory. Dzięki niej metoda staje się fabryką przypadków testowych.

Trzeba tu wziąć pod uwagę jeden aspekt dynamicznych testów. Wszystkie metody Before i After zostaną wywołane tylko raz, na początku i końcu metody-fabryki, zamiast dla każdego wykonanego testu. Ta właściwość testów dynamicznych sprawia, że nie można ich użyć, jeżeli potrzebna jest faza przygotowania danych i czyszczenia w testach. Jednak ze względu na to, że nasz walidator haseł nie potrzebuje żadnej dodatkowej konfiguracji, to możemy użyć dynamicznych testów do przetestowania go. Możemy to zrobić w następujący sposób:

internal class PasswordValidatorTest {

    private val validator = PasswordValidator()

    @TestFactory
    fun `given input password, when validating it, then is should return if it is valid`() =
        listOf(
            "Test123!" to true,
            "#tesT12!" to true,
            "12Es@t123" to true,
            "test123!" to false,
            "t " to false,
            "   " to false
        ).map { (password, expected) ->
            dynamicTest(
                "given \"$password\", " +
                        "when validating the password, " +
                        "then it should be reported as ${if (expected) "valid" else "invalid"}"
            ) {
                val actual = validator.isValid(password)
                assertThat(actual).isEqualTo(expected)
            }
        }
}


To podejście jest dla mnie łatwiejsze do napisania i zrozumienia. Dodajemy adnotację @TestFactory do naszej metody, tworzymy listę parametrów i używamy ich w testach naszego walidatora. Podoba mi się, że parametry i reszta testu są razem. Jeżeli mam więcej testów, które wymagają parametrów, to nie muszę przewijać na dół pliku, by znaleźć odpowiednią metodę. Co więcej, możesz bardziej dostosować nazwę testów, można więc się pozbyć true/false z nazwy testu i zastąpić je słowami valid i invalid, co lepiej opisuje nasz test.

Podsumowując, te dwie metody pozwalają na pozbycie się powtarzającego się kodu, poprzez napisanie parametryzowanych testów z użyciem JUnit 5 w Kotlinie. Obydwie metody mają swoje zalety i wady, sam zdecyduj co jest lepsze w Twoim przypadku. Jeżeli chcesz dowiedzieć się więcej na ten temat, zobacz oficjalną dokumentację. Ma ona sporo przykładów, które ułatwią poprawne używanie każdego podejścia.



Oryginał tekstu w języku angielskim przeczytasz tutaj.

<p>Loading...</p>