Testy jednostkowe w Androidzie cz. 3 – assert, assertThat, assert* czyli co powinieneś wiedzieć o asercjach.

Wstęp

Poświęcam cały artykuł asercji, ponieważ to ona decyduje o zakończeniu testu. Assert jest angielskim czasownikiem, który odpowiednikiem polskiego „zapewniać”. Słowo to świetnie oddaje jego znaczenie w testach. Jeśli „zapewniamy”, że coś jest równe 0 – assertEquals(0, actualValue) – to musi takie być, jeśli nie jest to test zostaje zakończony błędem.

Znajomość podstawowych asercji oraz biblioteki Hamcrest bardzo ułatwia pracę, dlatego w tym artykule zawarłem kompleksowe informacje na temat asercji w JUnit oraz Hamcrest.

Czego się nauczysz?

  • asercji w Kotlinie
  • standardowych asercji występujących w jUnit
  • tworzyć własne asercje
  • asercji w bibliotece Hamcrest
  • tworzenie własnej implementacji Matcher do assertThat

Czym jest assert?

W kotlinie jest to funkcja assert(value : Boolean) (w Javie słowo kluczowe assert), która w przypadku otrzymania wartości false rzuca wyjątek AssertionError – przerywa to działanie testu.

Zewnętrzne biblioteki dostarczają o wiele więcej asercji, zazwyczaj są one wyspecyfikowane, dzięki czemu zwiększają one czytelność testu i odciążają programistów od powtarzania kodu. Wszystkie jednak w przypadku niepowodzenia rzucają wyjątek AsserionError.

Przykłady w tym artykule

Aby przedstawić działanie asercji zastosuję tutaj testy uczące tzn. napiszę testy, które sprawdzają działanie zewnętrznych bibliotek. Jest to bardzo pożyteczna technika, ponieważ po zmianie wersji biblioteki możemy przetestować jej kompatybilność z poprzednią wersją. O tej technice przeczytałem w książce „Czysty Kod” Roberta C. Martina.

Kup książkę

Niektóre z testów zawierają adnotację @Test(expected = AssertionError::class) – stosuje się ją wtedy gdy oczekujemy, że wystąpi błąd. Jako expected może by podany dowolny wyjątek. Tutaj ze względu na to, że testuję wyłącznie asercje, to znajdziecie jedynie AssertionError:

    @Test(expected = AssertionError::class)
    fun testFailingAssertTrue() {
        assertTrue(false)
    }

Gdybyś w powyższym przykładzie nie użył adnotacji @Test(expected = AssertionError::class) test by nie przeszedł – mimo, że błąd w tym przypadku jest oczekiwany, ponieważ testujemy przypadki negatywne.

Asercje w Kotlinie

Kotlin dostarcza tylko dwie asercje, które są podstawą do tworzenia własnych.

assert(value : Boolean) przyjmuje wartość logiczną, jeśli wartość jest równa true to funkcja kończy się sukcesem, w innym przypadku przerywa działanie testu rzucając wyjątek AssertionError

    @Test
    fun testAssertion() {
        assert(true)
    }

    @Test(expected = AssertionError::class)
    fun testFailAssertion() {
        assert(false)
    }

assert(value : Boolean, lazyMessage : ()-> Any) – funkcja działa tak samo jak poprzednia, z tym że ma możliwość przekazania wiadomości, która wyświetli się w logach jeśli test zakończy się niepowodzeniem. Na przykład wywołanie funkcji assert(actualList.isEmpty()) nie niesie żadnych informacji i jeśli test się załamie to wiadomość będzie bardzo lakoniczna:

Jeśli dodasz wiadomość do tej asercji, to dostarczysz o wiele więcej informacji jeśli test zakończy się niepowodzeniem:

assert(actualList.isEmpty()) {"List should be empty but has ${actualList.size} elements"}
    @Test
    fun testFailAssertionWithMessage() {
        val expectedMessage = "This message show when test failed"
        try {
            assert(false) { expectedMessage }
        } catch (e: AssertionError) {
            assertEquals(expectedMessage, e.message)
            return
        }
        throw AssertionError("Assertion error expected")
    }

Własne asercje w Kotlinie

Aby stworzyć asercję wystarczy utworzyć nową funkcję, która sprawdza dowolny warunek za pomocą funkcji assert. Jako przykład stworzę asercję, która sprawdza czy podany ciąg znaków jest palindromem, czyli ciągiem znaków czytanym od tyłu tak samo jak od przodu. Przykładem takiego słowa jest „kajak”.

fun assertPalindrome(text: String) {
    assert(text.isNotBlank() && text.reversed() == text) {
        "\"$text\" is not palindrome.\"$text\" is not equal" +
                " to being reversed : \"${text.reversed()}\""
    }
}
class PalindromeTest {

    @Test(expected = AssertionError::class)
    fun emptyStringIsNotPalindrome() {
        assertPalindrome("")
    }

    @Test(expected = AssertionError::class)
    fun blankStringIsNotPalindrome() {
        assertPalindrome("      ")
    }

    @Test(expected = AssertionError::class)
    fun testNotPalindromeString() {
        assertPalindrome("not palindrome")
    }

    @Test
    fun testAssertPalindrome() {
        assertPalindrome("a")
        assertPalindrome("aa")
        assertPalindrome("aba")
        assertPalindrome("abcba")
        assertPalindrome("kajak")
    }
}

Jak zauważyłeś, użyłem funkcji assert(value : Boolean, lazyMessage : ()-> Any), która pozwala na dostarczenie dodatkowej wiadomości w przypadku gdy test nie przejdzie. Dzięki czemu mogę przekazać programistom dokładniejszą informację na temat błędu:

Asercje w JUnit 4

JUnit dostarcza więcej asercji niż Kotlin czy Java. Asercje zawarte w tej bibliotece są dobrze nazwane i wyspecjalizowane. Staraj się nie pisać własnych jeśli znajdziesz je w tej bibliotece. Zawsze używaj asercji, których nazwy precyzują ich działanie dzięki temu testy będą bardziej zrozumiałe i czytelne.

Wszystkie asercje przedstawione poniżej znajdują się w klasie org.junit.Assert jako metody statyczne. Tutaj znajdziesz kod źródłowy klasy Assert https://github.com/junit-team/junit4/blob/master/src/main/java/org/junit/Assert.java

Spis asercji

assertTrue(condition : Boolean)

assertTrue sprawdza czy wartość logiczna condition jest równa true. Jeśli jest równa false to test zakończy się błędem.

    @Test
    fun testAssertTrue() {
        assertTrue(true)
    }

    @Test(expected = AssertionError::class)
    fun testFailingAssertTrue() {
        assertTrue(false)
    }

assertFalse(condition : Boolean)

assertFalse sprawdza czy wartość logiczna condition jest równa false. Jeśli jest równa true to test zakończy się błędem.

    @Test
    fun testAssertFalse() {
        assertFalse(false)
    }

    @Test(expected = AssertionError::class)
    fun testFailingAssertFalse() {
        assertFalse(true)
    }

fail(message : String)

fail(String message) kończy test błędem z podaną wiadomością. Fail nie sprawdza czy wyrażenie jest prawdziwe – po prostu przerywa wykonywanie testu.

    @Test(expected = AssertionError::class)
    fun testFail() {
        fail("This should throw AssertionError")
    }

    @Test()
    fun testFailWithMessage() {
        val expectedMessage = "This should throw AssertionError"
        try {
            fail(expectedMessage)
        } catch (e: AssertionError) {
            assertEquals(e.message, expectedMessage)
            return
        }
        throw AssertionError("Assertion error expected")
    }

assertEquals(expected, actual)

assertEquals(expected, actual) sprawdza czy expected jest równe actual. expected oraz actual może być dowolnego typu, asercja ta do porównania wykorzystuje metodę equals. Jeśli expected nie jest równe actual test zakończy się błędem.

    @Test
    fun testAssertEquals() {
        val any = Any()
        assertEquals(any, any)
        assertEquals("some", "some")
        assertEquals(false, false)
        assertEquals(true, true)
        assertEquals(1, 1)
        assertEquals(1L, 1L)
        assertEquals(TestData("some", 1), TestData("some", 1))
    }

    @Test(expected = AssertionError::class)
    fun testFailingAssertEquals() {
        val any1 = Any()
        val any2 = Any()
        assertEquals(any1, any2)
        assertEquals("some", "some2")
        assertEquals(false, true)
        assertEquals(true, false)
        assertEquals(1, 2)
        assertEquals(1L, 2L)
        assertEquals(TestData("some", 2), TestData("some", 1))
    }

assertNotEquals(expected, actual)

assertNotEquals(expected, actual) sprawdza czy expected nie jest równeactual. expected oraz actual może być dowolnego typu, asercja ta do porównania wykorzystuje metodę equals. Jeśli expected i actual są równe test zakończy się błędem.

    @Test
    fun testAssertNotEquals() {
        val any1 = Any()
        val any2 = Any()
        assertNotEquals(any1, any2)
        assertNotEquals("some", "some2")
        assertNotEquals(false, true)
        assertNotEquals(true, false)
        assertNotEquals(1, 2)
        assertNotEquals(1L, 2L)
        assertNotEquals(TestData("some", 2), TestData("some", 1))
    }

    @Test(expected = AssertionError::class)
    fun testFailingAssertNotEquals() {
        val any = Any()
        assertNotEquals(any, any)
        assertNotEquals("some", "some")
        assertNotEquals(false, false)
        assertNotEquals(true, true)
        assertNotEquals(1, 1)
        assertNotEquals(1L, 1L)
        assertNotEquals(TestData("some", 1), TestData("some", 1))
    }

assertEquals(expected, actual, delta)

assertEquals(float expected, float actual, float delta) sprawdza czy 2 liczby zmiennoprzecinkowe są równe. Delta oznacza z jaką dokładnością chcesz porównać te dwie liczby. Jeśli actual różni się od expected więcej niż delta to test nie przejdzie. Więcej o dokładności liczb zmiennoprzecinkowych znajdziesz tutaj.

    @Test
    fun testAssertEqualsFloat() {
        assertEquals(1.1f, 1.1f, 0.0f)
        assertEquals(1.1, 1.1, 0.0)
        assertEquals(1.1001f, 1.1f, 0.001f)
        assertEquals(1.1001, 1.1, 0.001)
    }

    @Test(expected = AssertionError::class)
    fun testFailAssertEqualsFloat() {
        assertEquals(1.1001f, 1.1f, 0.0f)
        assertEquals(1.1001, 1.1, 0.0)
    }

assertNotEquals(expected, actual, delta)

assertNotEquals(float expected, float actual, float delta) sprawdza czy 2 liczby zmiennoprzecinkowe nie są równe. Delta oznacza z jaką dokładnością chcesz porównać te dwie liczby. Jeśli actual różni się od expected mniej niż delta to test nie przejdzie. Więcej o dokładności liczb zmiennoprzecinkowych znajdziesz tutaj.

    @Test(expected = AssertionError::class)
    fun testFailAssertNotEqualsFloat() {
        assertNotEquals(1.1f, 1.1f, 0.0f)
        assertNotEquals(1.1, 1.1, 0.0)
        assertNotEquals(1.1001f, 1.1f, 0.001f)
        assertNotEquals(1.1001, 1.1, 0.001)
    }

    @Test
    fun testAssertNotEqualsFloat() {
        assertNotEquals(1.1001f, 1.1f, 0.0f)
        assertNotEquals(1.1001, 1.1, 0.0)
    }

assertArrayEquals(expectedArray, actualArray)

assertArrayEquals(expectedArray, actualArray) sprawdza czy actualArray jest takiego samego rozmiaru, zawiera takie same elementy w tej samej kolejności jak expectedArray

    @Test
    fun testAssertArrayEquals() {
        val expectedArray = arrayOf(1, 2, 3, 4)
        val actualArray = arrayOf(1, 2, 3, 4)
        assertArrayEquals(expectedArray, actualArray)
    }

    @Test(expected = AssertionError::class)
    fun failingTestAssertArrayEquals() {
        val expectedArray = arrayOf(1, 2, 3, 4)
        val actualArray = arrayOf(1, 2, 4, 3)
        assertArrayEquals(expectedArray, actualArray)
    }

assertArrayEquals ma bardzo dużo wariacji i omówienie każdej z nich nie jest konieczne. Jeśli jesteś jednak zainteresowany dogłębnym zapoznaniem się z działaniem assertArrayEquals to wszystkie testy do tej metody znajdziesz tutaj: https://github.com/junit-team/junit4/blob/master/src/test/java/org/junit/tests/assertion/AssertionTest.java. Od linii 64 aż do 397 znajdują się testy dla tej metody. Mimo, że assertArrayEquals wydaje się proste to istnieje bardzo dużo przypadków, które funkcja ta musi pokryć np. wielowymiarowe tablice.

assertNotNull(object)

assertNotNull sprawdza czy object nie jest równy null. Jeśli object jest nullem to test zakończy się błędem.

    @Test
    fun testAssertNotNull() {
        assertNotNull(Any())
    }


    @Test(expected = AssertionError::class)
    fun testFailAssertNotNull() {
        assertNotNull(null)
    }

assertNull(object)

assertNull sprawdza czy object jest równy null. Jeśli object nie jest nullem to test zakończy się błędem.

    @Test
    fun testAssertNull() {
        assertNull(null)
    }

    @Test(expected = AssertionError::class)
    fun testFailAssertNull() {
        assertNull(Any())
    }

assertSame(expected, actual)

assertSame(expected, actual) sprawdza czy actual jest tym samym obiektem co expected – czyli czy te dwie zmienne mają taką samą referencję. Jeśli actual nie jest tym samym obiektem co expected to test zakończy się błędem.

    @Test
    fun testFailingAssertNotSame() {
        assertNotSame(TestData("some", 1), TestData("some", 1))
    }

    @Test(expected = AssertionError::class)
    fun testAssertNotSame() {
        val testData = TestData("some", 1)
        assertNotSame(testData, testData)
    }

assertNotSame(expected, actual)

assertNotSame(expected, actual) sprawdza czy actual nie jest tym samym obiektem co expected – czyli czy te dwie zmienne mają różną referencję. Jeśli actual jest tym samym obiektem co expected to test zakończy się błędem.

    @Test(expected = AssertionError::class)
    fun testFailingAssertSame() {
        assertSame(TestData("some", 1), TestData("some", 1))
    }

    @Test
    fun testAssertSame() {
        val testData = TestData("some", 1)
        assertSame(testData, testData)
    }

Assercje w Hamcrest

Hamcrest nie znalazł się tu przypadkowo, jest to najpopularniejsza biblioteka zawierająca rozszerzalny i uniwersalny mechanizm asercji. Biblioteka jest rozwijana od ponad 10 lat i nadal wpierana.

Konfiguracja w projekcie

dependencies {
    testImplementation "org.hamcrest:hamcrest:2.2"
}

Używanie

Hamcrest dostarcza asercje w trochę innej formie niż JUnit. Hamcrest dostarcza „jedną” uniwersalną metodę assertThat(actual, matcher). Pierwszy argument przyjmuje obiekt, który będzie testowany, drugim argumentem jest obiekt typu Matcher. Matcher jest interfejsem, którego implementacją mają za zadanie sprawdzić poprawność obiektu actual.

    @Test
    fun testEqualTo() {
        val a = 1
        assertThat(a, equalTo(1))
    }

Powyższy kod na przykład sprawdza, czy zmienna a jest równa 1. Interfejs Matcher można rozszerzać w dowolny sposób, co daje nieskończoną możliwość tworzenia własnych asercji i korzystania z wszystkich udogodnień biblioteki Hamcrest.

Wszystkie wbudowane w bibliotekę asercje są tworzone za pomocą metod wytwórczych (Factory Method) takich jak equalTo

    public static <T> Matcher<T> equalTo(T operand) {
        return new IsEqual<>(operand);
    }

equalTo tworzy obiekt klasy IsEqual. Klasa IsEqual rozszerza klasę BaseMatcher:

public class IsEqual<T> extends BaseMatcher<T> {
   //...
}

a BaseMacher implementuje interfejs Matcher:

public abstract class BaseMatcher<T> implements Matcher<T> {
   //...
}

Tak wygląda klasyczna implementacja prawdopodobnie każdego matchera w tej bibliotece.

Matchery dla obiektów

equalTo(operand)

sprawdza czy testowany obiekt jest równy obiektowi operand. Test równości jest wykonywany za pomocą metody equals(). Jeśli chcesz sprawdzić czy testowany obiekt jest tym samym obiektem co expected to użyj sameInstance

    @Test
    fun testEqualTo() {
        assertThat(1, equalTo(1))
        assertThat("abc", equalTo("abc"))
        assertThat("abc", equalTo("abc"))
        assertThat(true, equalTo(true))
        assertThat(false, equalTo(false))
        assertThat(TestData("some", 1), equalTo(TestData("some", 1)))
    }

    @Test(expected = AssertionError::class)
    fun testFailingEqualTo() {
        assertThat(1, equalTo(2))
    }

instanceOf(type)

sprawdza czy testowany obiekt jest typu type.

    @Test
    fun testInstanceOf() {
        val testData: Thing = SubtypeThingA()
        assertThat(testData, instanceOf(SubtypeThingA::class.java))
    }

    @Test(expected = AssertionError::class)
    fun testFailingInstanceOf() {
        val testData: Thing = SubtypeThingA()
        assertThat(testData, instanceOf(SubtypeThingB::class.java))
    }

not(value), not(matcher)

sprawdza czy testowana wartość nie jest równa drugiej wartości assertThat(1, not(2). Lub zaprzecza wynik innego matchera np. assertThat(testData, not(instanceOf(SubtypeThingB::class.java))) – sprawdza czy obiekt testData nie jest obiektem klasy SubtypeThingB.

    @Test
    fun testNot() {
        assertThat(1, not(2))
        assertThat(1, not(equalTo(2)))
        assertThat("abc", not("bca"))
        val testData: Thing = SubtypeThingA()
        assertThat(testData, not(instanceOf(SubtypeThingB::class.java)))
    }

    @Test(expected = AssertionError::class)
    fun testFailingNot() {
        assertThat(1, not(1))
    }

notNullValue()

Sprawdza czy testowany obiekt nie jest równy null.

    @Test
    fun testNotNullValue() {
        assertThat(SubtypeThingA(), notNullValue())
    }

    @Test(expected = AssertionError::class)
    fun testFailingNotNullValue() {
        assertThat(null, notNullValue())
    }

nullValue()

Sprawdza czy testowany obiekt jest równy null.

    @Test
    fun testNullValue() {
        assertThat(null, nullValue())
    }

    @Test(expected = AssertionError::class)
    fun testFailingNullValue() {
        assertThat(SubtypeThingA(), nullValue())
    }

sameInstance(target)

Sprawdza czy testowany obiekt jest tym samym obiektem co target

    @Test
    fun testSameInstance() {
        val testData = TestData("a", 1)
        val testData2 = testData
        assertThat(testData, sameInstance(testData2))
    }

    @Test(expected = AssertionError::class)
    fun testFailingSameInstance() {
        val testData = TestData("a", 1)
        val testData2 = TestData("a", 1)
        assertThat(testData, sameInstance(testData2))
    }

is

Skrót do equalTo – w kotlinie nie polecam używać, ponieważ trzeba objąć słowo is w apostrof ponieważ słowo is jest słowem kluczowym w kotlinie.

    @Test
    fun testIs() {
        assertThat(1, `is`(1))
    }

    @Test(expected = AssertionError::class)
    fun testFailingIs() {
        assertThat(1, `is`(2))
    }

in(collection)

Sprawdza czy testowany obiekt znajduje się w podanej liście collection

    @Test
    fun testIn(){
        val list = listOf(1,2,3,4)
        assertThat(1, `in`(list))
    }

    @Test(expected = AssertionError::class)
    fun testFailingIn(){
        val list = listOf(1,2,3,4)
        assertThat(5, `in`(list))
    }

oneOf(elements)

Sprawdza czy testowany obiekt znajduje się w podanej liście elements.

    @Test
    fun testOneOf(){
        assertThat(1, oneOf(1,2,3,4))
    }

    @Test(expected = AssertionError::class)
    fun testFailingOneOf(){
        assertThat(5, oneOf(1,2,3,4))
    }

closeTo(operand, error)

Sprawdza czy testowana liczba zmiennoprzecinkowa jest zbliżona do liczby operand z uwzględnieniem błędu error

    @Test
    fun testCloseTo(){
        assertThat(1.0001, closeTo(1.0, 0.01))
    }


    @Test(expected = AssertionError::class)
    fun testFailingCloseTo(){
        assertThat(1.101, closeTo(1.0, 0.01))
    }

notANumber()

Sprawdza czy testowana liczba zmiennoprzecinkowa nie jest liczbą – czyli sprawdza czy jest równa Double.NaN

    @Test
    fun testNotANumber(){
        assertThat(Double.NaN, notANumber())
    }

    @Test(expected = AssertionError::class)
    fun testFailingNotANumber(){
        assertThat(1.0, notANumber())
    }

comparesEqualTo(value)

Sprawdza czy testowany obiekt (lub wartość) jest równy value. Matcher wykorzystuje do sprawdzenia metodę compareTo zamiast metody equals. Jest ona użyteczna jeśli porównuje się 2 obiekty, które implementują interfejs Comparable ale nie są równe w sensie logicznym.

    class Tank(
        val name : String,
        val power : Int
    ) : Comparable<Tank>{
        override fun compareTo(other: Tank): Int {
            return when {
                other.power == power -> 0
                other.power > power -> -1
                else -> 1
            }
        }
    }

    @Test
    fun testComparesEqualTo(){
        val sherman1 = Tank("sherman1", 100)
        val sherman2 = Tank("sherman2", 100)
        assertThat(sherman1, comparesEqualTo(sherman2))
    }

    @Test(expected = AssertionError::class)
    fun testFailComparesEqualTo(){
        val panther = Tank("panther", 300)
        val sherman = Tank("sherman", 100)
        assertThat(sherman, comparesEqualTo(panther))
    }

greaterThan(value)

Sprawdza czy testowany obiekt jest większy od drugiego. Jeśli obiekt nie jest liczbą to jest wykorzystywana metoda compareTo interfejsu Comparable

    @Test
    fun testGreaterThan() {
        val panther = Tank("panther", 300)
        val sherman = Tank("sherman", 100)
        assertThat(panther, greaterThan(sherman))
    }

    @Test(expected = AssertionError::class)
    fun testFailingGreaterThan() {
        val sherman1 = Tank("sherman1", 100)
        val sherman2 = Tank("sherman2", 100)
        assertThat(sherman1, greaterThan(sherman2))
    }

greaterThanOrEqualTo(value)

Sprawdza czy testowany obiekt jest większy lub równy od drugiego. Jeśli obiekt nie jest liczbą to jest wykorzystywana metoda compareTo interfejsu Comparable

    @Test
    fun testGreaterThanOrEqualTo(){
        val panther = Tank("panther", 300)
        val panther2 = Tank("panther2", 300)
        val sherman = Tank("sherman1", 100)
        assertThat(panther, greaterThanOrEqualTo(sherman))
        assertThat(panther, greaterThanOrEqualTo(panther2))
    }

    @Test(expected = AssertionError::class)
    fun testFailingGreaterThanOrEqualTo(){
        val panther = Tank("panther", 300)
        val sherman = Tank("sherman1", 100)
        assertThat(sherman, greaterThanOrEqualTo(panther))
    }

lessThan(value)

Sprawdza czy testowany obiekt jest mniejszy od drugiego. Jeśli obiekt nie jest liczbą to jest wykorzystywana metoda compareTo interfejsu Comparable

    @Test
    fun testLessThan(){
        val panther = Tank("panther", 300)
        val sherman = Tank("sherman", 100)
        assertThat(panther, greaterThan(sherman))
    }

    @Test(expected = AssertionError::class)
    fun testFailingLessThan(){
        val sherman1 = Tank("sherman1", 100)
        val sherman2 = Tank("sherman2", 100)
        assertThat(sherman1, greaterThan(sherman2))
    }

lessThanOrEqualTo(value)

Sprawdza czy testowany obiekt jest mniejszy lub równy od drugiego. Jeśli obiekt nie jest liczbą to jest wykorzystywana metoda compareTo interfejsu Comparable

    @Test
    fun testLessThanOrEqualTo(){
        val panther = Tank("panther", 300)
        val panther2 = Tank("panther2", 300)
        val sherman = Tank("sherman1", 100)
        assertThat(sherman, lessThanOrEqualTo(panther))
        assertThat(panther, lessThanOrEqualTo(panther2))
    }

    @Test(expected = AssertionError::class)
    fun testFailingLessThanOrEqualTo(){
        val panther = Tank("panther", 300)
        val sherman = Tank("sherman1", 100)
        assertThat(panther, lessThanOrEqualTo(sherman))
    }

typeCompatibleWith(baseType)

Sprawdza czy testowana klasa jest pochodną klasy baseType.

    @Test
    fun testTypeCompatibleWith() {
        val number = Integer(1)
        assertThat(number::class.java, typeCompatibleWith(Number::class.java))
        assertThat(Integer::class.java, typeCompatibleWith(Number::class.java))
    }

    @Test(expected = AssertionError::class)
    fun testFailTypeCompatibleWith() {
        assertThat(String::class.java, typeCompatibleWith(Number::class.java))
    }

Matchery dla ciągów znaków

containsString(substring)

Sprawdza czy testowany ciąg znaków zawiera w sobie ciąg znaków substring

    @Test
    fun testContainsString() {
        assertThat("Far Far away", containsString("away"))
    }

    @Test(expected = AssertionError::class)
    fun testFailingContainsString() {
        assertThat("Far Far away", containsString("Away"))
    }

containsStringIgnoringCase(substring)

Sprawdza czy testowany ciąg znaków zawiera w sobie ciąg znaków substring bez względu na wielkość liter.

    @Test
    fun testContainsStringIgnoringCase() {
        assertThat("Far Far away", containsStringIgnoringCase("Away"))
    }

    @Test(expected = AssertionError::class)
    fun testFailingContainsStringIgnoringCase() {
        assertThat("Far Far away", containsStringIgnoringCase("house"))
    }

startsWith(prefix)

Sprawdza czy testowany ciąg znaków zaczyna się odprefix

    @Test
    fun testStartsWith() {
        assertThat("Far Far away", startsWith("Far"))
    }

    @Test(expected = AssertionError::class)
    fun testFailingStartsWith() {
        assertThat("Far Far away", startsWith("fAr"))
    }

startsWithIgnoringCase(substring)

Sprawdza czy testowany ciąg znaków zaczyna się odprefix bez względu na wielkość liter

  @Test
    fun testStartsWithIgnoringCase() {
        assertThat("Far Far away", startsWithIgnoringCase("fAr"))
    }

    @Test(expected = AssertionError::class)
    fun testFailingStartsWithIgnoringCase() {
        assertThat("Far Far away", startsWithIgnoringCase("Away"))
    }

endsWith(suffix)

Sprawdza czy testowany ciąg znaków kończy się na suffix

    @Test
    fun testEndsWith() {
        assertThat("Far Far away", endsWith("away"))
    }

    @Test(expected = AssertionError::class)
    fun testFailingEndsWith() {
        assertThat("Far Far away", endsWith("Away"))
    }

endsWithIgnoringCase(suffix)

Sprawdza czy testowany ciąg znaków kończy się na suffix bez względu na wielkość liter

    @Test
    fun testEndsWithIgnoringCase() {
        assertThat("Far Far away", endsWithIgnoringCase("AwAy"))
    }

    @Test(expected = AssertionError::class)
    fun testFailingEndsWithIgnoringCase() {
        assertThat("Far Far away", endsWithIgnoringCase("far"))
    }

matchRegex(regex)

Sprawdza czy testowany ciąg znaków pasuje do podanego wyrażenia regularnego regex

    @Test
    fun testMatchRegex() {
        val lineWithNumberPattern = "\\d*[.]\\d*.*"
        val text = "1.2 some text with number"
        assertThat(text, matchesRegex(lineWithNumberPattern))
    }

    @Test(expected = AssertionError::class)
    fun testFailingMatchRegex() {
        val lineWithNumberPattern = "\\d*[.]\\d*.*"
        val text = "some text without number"
        assertThat(text, matchesRegex(lineWithNumberPattern))
    }

matchesPattern(pattern)

Działa podobnie do matchesRegex – sprawdza czy testowany ciąg znaków pasuje do podanego wyrażenia regularnego ale wykorzystuje klasę Pattern. Daje to możliwość zastosowania flag np. domyślnie znak . w wyrażeniu regularnym oznacza każdy znak, za wyjątkiem \n. Jeśli skorzystasz z flagi Pattern.DOTALL to znak . będzie też oznaczał znak końca linii.

    @Test
    fun testMatchesPattern() {
        val lineWithNumberPattern = "\\d*[.]\\d*.*"
        val text = "1.2 some text with number\n"
        assertThat(text,  matchesPattern(Pattern.compile(lineWithNumberPattern, Pattern.DOTALL)))
    }

    @Test(expected = AssertionError::class)
    fun testFailingMatchesPattern() {
        val lineWithNumberPattern = "\\d*[.]\\d*.*"
        val text = "1.2 some text with number\n"
        assertThat(text,  matchesPattern(Pattern.compile(lineWithNumberPattern)))
    }

equalTo(operand)

Sprawdza czy testowany ciąg znaków jest taki sam jak operand

    @Test
    fun testEqualToString() {
        assertThat("abc", equalTo("abc"))
    }

    @Test(expected = AssertionError::class)
    fun testFailingEqualToString() {
        assertThat("abc", equalTo("ABC"))
    }

equalToIgnoringCase(expectedString)

Sprawdza czy testowany ciąg znaków jest taki sam jak expectedString bez względu na wielkość liter.

    @Test
    fun testEqualToIgnoringCase() {
        assertThat("abc", equalToIgnoringCase("ABC"))
    }

    @Test(expected = AssertionError::class)
    fun testFailingEqualToIgnoringCase() {
        assertThat("abc", equalToIgnoringCase("DEF"))
    }

equalToCompressingWhiteSpace(expectedString)

Sprawdza czy testowany ciąg znaków jest taki sam jak expectedString ignorując białe znaki na początku i na końcu tekstu.

    @Test
    fun testEqualToCompressingWhiteSpace() {
        assertThat("   abc ", equalToCompressingWhiteSpace("abc"))
    }

    @Test(expected = AssertionError::class)
    fun testFailingEqualToCompressingWhiteSpace() {
        assertThat(" a b c  ", equalToCompressingWhiteSpace("abc"))
    }

emptyOrNullString

Sprawdza czy testowany ciąg znaków jest pusty lub równy null

    @Test
    fun testEmptyOrNullString() {
        assertThat("",  emptyOrNullString())
        assertThat(null,  emptyOrNullString())
    }

    @Test(expected = AssertionError::class)
    fun testFailingEmptyOrNullString() {
        assertThat("  ",  emptyOrNullString())
    }

emptyString

Sprawdza czy testowany ciąg znaków jest pusty

    @Test
    fun testEmptyString() {
        assertThat("",  emptyString())
    }

    @Test(expected = AssertionError::class)
    fun testFailingEmptyString() {
        assertThat(null,  emptyString())
    }

blankOrNullString

Sprawdza czy testowany ciąg znaków jest pusty, składa się z samych białych znaków albo jest równy null

    @Test
    fun testBlankOrNullString() {
        assertThat("",  blankOrNullString())
        assertThat("    ",  blankOrNullString())
        assertThat("\n",  blankOrNullString())
        assertThat(null,  blankOrNullString())
    }

    @Test(expected = AssertionError::class)
    fun testFailingBlankOrNullString() {
        assertThat("  .",  blankOrNullString())
    }

blankString

Sprawdza czy testowany ciąg znaków jest pusty lub składa się z samych białych znaków

    @Test
    fun testBlankString() {
        assertThat("",  blankString())
        assertThat("    ",  blankString())
        assertThat("\n",  blankString())
    }

    @Test(expected = AssertionError::class)
    fun testFailingBlankString() {
        assertThat(null,  blankString())
    }

stringContainsInOrder(substrings)

Sprawdza czy testowany ciąg znaków zawiera ciągi w sobie ciągi znaków w tej samej kolejności co substrings

    @Test
    fun testStringContainsInOrder() {
        assertThat("1.abc 2.def 3.ghi", stringContainsInOrder("1.", "2.", "3."))
    }

    @Test(expected = AssertionError::class)
    fun testFailingStringContainsInOrder() {
        assertThat("2.def 1.abc 3.ghi", stringContainsInOrder("1.", "2.", "3."))
    }

hasLength(length)

Sprawdza czy testowany ciąg znaków ma długość równą length

    @Test
    fun testHasLength() {
        assertThat("abc", hasLength(3))
    }

    @Test(expected = AssertionError::class)
    fun testFailingHasLength() {
        assertThat("abc", hasLength(0))
    }

Matchery dla kolekcji i tablic

everyItem(itemMatcher)

sprawdza czy wszystkie elementy tablicy przejdą test za pomocą matchera przekazanego jako itemMatcher

    @Test
    fun testEveryItem() {
        val list = listOf(1, 2, 3, 4, 5, 6)
        assertThat(list, everyItem(lessThan(10)))
    }

    @Test(expected = AssertionError::class)
    fun testFailEveryItem() {
        val list = listOf(1, 2, 3, 4, 5, 6)
        assertThat(list, everyItem(lessThan(5)))
    }

hasItem(item)

Sprawdza czy testowana kolekcja zawiera element item

    @Test
    fun testHasItem() {
        val list = listOf(1, 2, 3, 4, 5, 6)
        assertThat(list, hasItem(2))
    }

    @Test(expected = AssertionError::class)
    fun testFailingHasItem() {
        val list = listOf(1, 2, 3, 4, 5, 6)
        assertThat(list, hasItem(10))
    }

hasItems(items)

Sprawdza czy testowana kolekcja zawiera w sobie elementy items

@Test
fun testHasItems() {
    val list = listOf(1, 2, 3, 4, 5, 6)
    assertThat(list, hasItems(2, 5, 6))
}

@Test(expected = AssertionError::class)
fun testFailingHasItems() {
    val list = listOf(1, 2, 3, 4, 5, 6)
    assertThat(list, hasItems(2, 5, 10))
}

array(elementMatchers)

Tworzy tablicę matcherów do każdego elementu testowanej tablicy. Ilość matcherów musi być taka sama jak ilość elementów w tablicy. Test zostanie zakończony sukcesem tylko wtedy gdy każdy matcher zakończy się bez błędu.

    @Test
    fun testArray() {
        val array = arrayOf(1, 10, 50)
        assertThat(array, Matchers.array(equalTo(1), lessThan(20), greaterThan(40)))
    }

    @Test(expected = AssertionError::class)
    fun testFailingArray() {
        val array = arrayOf(1, 25, 30)
        assertThat(array, Matchers.array(equalTo(1), lessThan(20), greaterThan(40)))
    }

hasItemInArray(element)

Sprawdza czy testowana tablica zawiera element

    @Test
    fun testHasItemInArray() {
        val array = arrayOf("a", "b", "c", "d")
        assertThat(array, hasItemInArray("c"))
    }

    @Test(expected = AssertionError::class)
    fun testFailingHasItemInArray() {
        val array = arrayOf("a", "b", "c", "d")
        assertThat(array, hasItemInArray("f"))
    }
    @Test
    fun testHasItemInArrayNumbers() {
        val array = arrayOf(1, 2, 3, 4, 5)
        assertThat(array, hasItemInArray(greaterThan(4)))
    }

    @Test(expected = AssertionError::class)
    fun testFailingHasItemInArrayNumbers() {
        val array = arrayOf(1, 2, 3, 4, 5)
        assertThat(array, hasItemInArray(greaterThan(9)))
    }

arrayContaining(items)

Sprawdza czy testowana tablica zawiera dokładnie takie same elementy i z tą samą kolejnością jak items

    @Test
    fun testArrayContaining() {
        val array = arrayOf("a", "b", "c", "d")
        assertThat(array, arrayContaining("a", "b", "c", "d"))
    }

    @Test(expected = AssertionError::class)
    fun testFailingArrayContaining() {
        val array = arrayOf("a", "b", "c", "d")
        assertThat(array, arrayContaining("a", "b"))
    }

    @Test
    fun testArrayContainingNumbers() {
        val array = arrayOf(1, 7, 30)
        assertThat(array, arrayContaining(
                greaterThan(0),
                greaterThan(5),
                greaterThan(10)
        ))
    }

    @Test(expected = AssertionError::class)
    fun testFailingArrayContainingNumbers() {
        val array = arrayOf(1, 2, 3)
        assertThat(array, arrayContaining(
                greaterThan(5),
                greaterThan(0),
                greaterThan(10)
        ))
    }

arrayContainingInAnyOrder(items)

Sprawdza czy testowana tablica zawiera takie same elementy jak items ignorując kolejność.

    @Test
    fun testArrayContainingInAnyOrder() {
        val array = arrayOf("a", "b", "c", "d")
        assertThat(array, arrayContainingInAnyOrder("b", "a", "c", "d"))
    }

    @Test(expected = AssertionError::class)
    fun testFailingArrayContainingInAnyOrder() {
        val array = arrayOf("a", "b", "c", "d")
        assertThat(array, arrayContainingInAnyOrder("a", "z", "c", "d"))
    }

    @Test
    fun testArrayContainingInAnyOrderNumbers() {
        val array = arrayOf(1, 7, 30)
        assertThat(array, arrayContainingInAnyOrder(
                greaterThan(5),
                greaterThan(0),
                greaterThan(10)
        ))
    }

    @Test(expected = AssertionError::class)
    fun testFailingArrayContainingInAnyOrderNumbers() {
        val array = arrayOf(1, 2, 3)
        assertThat(array, arrayContainingInAnyOrder(
                greaterThan(0),
                greaterThan(5),
                greaterThan(10)
        ))
    }

arrayWithSize(size)

Sprawdza czy testowana tablica ma rozmiar size.

    @Test
    fun testArrayWithSize() {
        val array = arrayOf(1, 2, 3)
        assertThat(array, arrayWithSize(3))
        assertThat(array, arrayWithSize(lessThan(5)))
        assertThat(array, arrayWithSize(greaterThan(2)))
    }

    @Test(expected = AssertionError::class)
    fun testFailingArrayWithSize() {
        val array = arrayOf(1, 2, 3)
        assertThat(array, arrayWithSize(lessThan(3)))
    }

emptyArray

Sprawdza czy testowana tablica jest pusta.

    @Test
    fun testEmptyArray() {
        val array = arrayOf<Int>()
        assertThat(array, emptyArray())
    }

    @Test(expected = AssertionError::class)
    fun testFailingEmptyArray() {
        val array = arrayOf(1, 2, 3)
        assertThat(array, emptyArray())
    }

hasSize(size)

Sprawdza czy testowana kolekcja ma rozmiar równy size

    @Test
    fun testHasSize() {
        val collection = listOf(1, 2, 3)
        assertThat(collection, hasSize(3))
        assertThat(collection, hasSize(lessThan(5)))
        assertThat(collection, hasSize(greaterThan(2)))
    }

    @Test(expected = AssertionError::class)
    fun testFailingHasSize() {
        val collection = listOf(1, 2, 3)
        assertThat(collection, hasSize(lessThan(3)))
    }

empty

Sprawdza czy testowana kolekcja jest pusta.

    @Test
    fun testEmpty() {
        val collection = listOf<Any>()
        assertThat(collection, empty())
    }

    @Test(expected = AssertionError::class)
    fun testFailingEmpty() {
        val collection = listOf(1, 2, 3)
        assertThat(collection, empty())
    }

contains(items)

Sprawdza czy testowana kolekcja zawiera dokładnie takie same elementy i z tą samą kolejnością co items

    @Test
    fun testContains() {
        val collection = listOf(1, 2, "a", "b")
        assertThat(collection, contains(1, 2, "a", "b"))
    }

    @Test(expected = AssertionError::class)
    fun testFailingContains() {
        val collection = listOf(1, 2, "a", "b")
        assertThat(collection, contains("a", "b", 1, 2))
    }

containsInAnyOrder(items)

Sprawdza czy testowana kolekcja zawiera dokładnie takie same elementy jak items ignorując kolejność.

    @Test
    fun testContainsInAnyOrder() {
        val collection = listOf(1, 2, "a", "b")
        assertThat(collection, containsInAnyOrder("a", "b", 1, 2))
    }

    @Test(expected = AssertionError::class)
    fun testFailingContainsInAnyOrder() {
        val collection = listOf(1, 2, "a", "b")
        assertThat(collection, containsInAnyOrder("a", 1))
    }

containsInRelativeOrder(items)

Sprawdza czy testowana kolekcja zawiera wszystkie elementy z items z zachowaniem relatywnej kolejności tzn. pomiędzy wskazanymi elementami mogą być inne elementy, ważne aby kolejność wystąpienia była prawidłowa.

   @Test
    fun testContainsInRelativeOrder() {
        val collection = listOf(1, 2, "a", "b")
        assertThat(collection, containsInRelativeOrder(1, "b"))
    }

    @Test(expected = AssertionError::class)
    fun testFailingContainsInRelativeOrder() {
        val collection = listOf(1, 2, "a", "b")
        assertThat(collection, containsInRelativeOrder("a", 1))
    }

Matchery dla Map

aMapWithSize(size)

Sprawdza czy testowana mapa ma rozmiar równy size

    @Test
    fun testAMapWithSize(){
        val map = mapOf(
                "a" to 1,
                "b" to 2,
                "c" to 3
        )
        assertThat(map, aMapWithSize(3))
    }

    @Test(expected = AssertionError::class)
    fun testFailingAMapWithSize(){
        val map = mapOf(
                "a" to 1,
                "b" to 2,
                "c" to 3
        )
        assertThat(map, aMapWithSize(1))
    }

anEmptyMap

Sprawdza czy testowana mapa jest pusta.

    @Test
    fun testAnEmptyMap(){
        val map = mapOf<String, Int>()
        assertThat(map, anEmptyMap())
    }

    @Test(expected = AssertionError::class)
    fun testFailingAnEmptyMap(){
        val map = mapOf(
                "a" to 1,
                "b" to 2,
                "c" to 3
        )
        assertThat(map, anEmptyMap())
    }

hasEntry(key, value)

Sprawdza czy testowana mapa posiada parę key value

    @Test
    fun testHasEntry(){
        val map = mapOf(
                "a" to 1,
                "b" to 2,
                "c" to 3
        )        
        assertThat(map, hasEntry("a", 1))
    }

    @Test(expected = AssertionError::class)
    fun testFailingHasEntry(){
        val map = mapOf(
                "a" to 1,
                "b" to 2,
                "c" to 3
        )
        assertThat(map, hasEntry("d", 4))
    }

hasKey(key)

Sprawdza czy testowana mapa posiada klucz key

    @Test
    fun testHasKey(){
        val map = mapOf(
                "a" to 1,
                "b" to 2,
                "c" to 3
        )
        assertThat(map, hasKey("a"))
    }

    @Test(expected = AssertionError::class)
    fun testFailingHasKey(){
        val map = mapOf(
                "a" to 1,
                "b" to 2,
                "c" to 3
        )
        assertThat(map, hasKey("d"))
    }

hasValue(value)

Sprawdza czy testowana mapa posiada value

    @Test
    fun testHasValue(){
        val map = mapOf(
                "a" to 1,
                "b" to 2,
                "c" to 3
        )
        assertThat(map, hasValue(1))
    }

    @Test(expected = AssertionError::class)
    fun testFailingHasValue(){
        val map = mapOf(
                "a" to 1,
                "b" to 2,
                "c" to 3
        )
        assertThat(map, hasValue(4))
    }

Grupowanie logiczne matcherów

Wśród matcherów występują takie, które pozwalają tworzyć wyrażenia logiczne poprzez grupowanie innych matcherów. Na przykład, zamiast sprawdzać czy liczba jest większa od 50 i mniejsza od 100 za pomocą 2 asercji,

 assertThat(number, greaterThan(50))
 assertThat(number, lessThan(100))

można wykorzystać matcher allOf. Kończy się on sukcesem jeśli wszystkie matchery poprawnie zweryfikują testowany obiekt lub wartość:

assertThat(number, allOf(greaterThan(50), lessThan(100)))

Powyższy kod weryfikuje czy number jest większy od 50 i jednocześnie mniejszy od 100.

allOf(matcher...)

Weryfikuje czy wszystkie matchery podane jako argument kończą się sukcesem. Jeśli choć jeden z nich nie zakończy się sukcesem to test zostanie przerwany.

   @Test
    fun testAllOf() {
        assertThat("abcde", allOf(startsWith("a"), endsWith("e"), containsString("de")))
    }

    @Test(expected = AssertionError::class)
    fun testFailingAllOf() {
        assertThat("abcde", allOf(startsWith("b"), endsWith("e"), containsString("de")))
    }

both and or

Weryfikuje czy wszystkie matchery podane jako argument kończą się sukcesem. Jeśli choć jeden z nich nie zakończy się sukcesem to test zostanie przerwany. Ma podobne działanie jak allOf lecz inną konstrukcję – zamiast przekazywania listy matcherów tworzy się warunek logiczny poprzez kaskadowe wywołanie metod: both(greaterThan(10)).and(lessThan(40)). Wykorzystanie metody and do łączenia kilku matcherów pozwala na stworzenie bardziej czytelnego kodu.

    @Test
    fun testBoth() {
        assertThat(30, both(greaterThan(10)).and(lessThan(40)))
    }

    @Test(expected = AssertionError::class)
    fun testFailingBoth() {
        assertThat(30, both(greaterThan(10)).and(lessThan(20)))
    }

Można trochę rozbudować powyższy przykład o funkcję logiczną or lub and i tworzyć bardziej skomplikowane konstrukcje:

    @Test
    fun testBothAndOr() {
        assertThat(65,
                both(lessThan(10))
                        .and(lessThan(40))
                        .or(lessThan(70))
                        .and(greaterThan(60)))
    }

Nie należy przesadzać z takimi konstrukcjami, ponieważ zastosowanie wielokrotnie and oraz or może zmniejszyć czytelność.

either

Weryfikuje czy przynajmniej jeden matcher podany jako argument kończy się sukcesem. Test zostanie przerwany tylko i wyłącznie jeśli wszystkie matchery zakończą się błędem. Konstruuje się go wykorzystując metodę or a następnie tak jak w both można mieszać and oraz or

    @Test
    fun testEither() {
        assertThat(30, either(greaterThan(40)).or(lessThan(35)))
    }

    @Test(expected = AssertionError::class)
    fun testFailingEither() {
        assertThat(30, either(greaterThan(40)).or(lessThan(20)))
    }

Tworzenie własnego matchera

Na zam koniec to co najlepsze, czyli stworzenie swojego własnego matchera. Często w aplikacjach zachodzi potrzeba zbudowania bardziej skomplikowanej asercji lub wielu asercji, które można byłoby ubrać w jeden matcher, który potem można byłoby używać w całej aplikacji.

Aby porównać dwa podejścia tworzenia asercji, posłużę się asercją sprawdzającą czy tekst jest palindromem z początku artykułu. Zwykła asercja ma taką postać:

fun assertPalindrome(text: String) {
    assert(text.reversed() == text) {
        "Text [$text] is not palindrome. Compare:" +
                "\n$text <- normal" +
                "\n${text.reversed()} <-reversed"
    }
}

Aby stworzyć jej odpowiednik w formie Matchera należy stworzyć klasę, która rozszerza klasę TypeSafeMatcher. To nam daje pewność, że nasz matcher operuje tylko na klasie String:

class IsPalindrome : TypeSafeMatcher<String>(){
    override fun describeTo(description: Description) {
        
    }

    override fun matchesSafely(item: String?): Boolean {
        
    }
}

Klasa TypeSafeMatcher jest klasą abstrakcyjną i ma 2 metody abstrakcyjne.

describeTo(description: Description) – w niej przekazujemy opis dotyczący wymaganej wartości podczas testu. Gdy test się załamie zostanie wyświetlony w konsoli. Klasa Descrption jest builderem opisu, aby dodać do niego tekst należy wywołać metodę appendText("a palindrome string"). W przypadku wystąpienia błędu pojawi się poniższy błąd:

matchesSafely(item: String?) – ta metoda jest najważniejsza w Matcherze, tutaj odbywa się sprawdzenie poprawności testowanej wartości. Jeśli metoda zwróci false to test zakończy się błędem, jeśli true to test przejdzie pomyślnie.

class IsPalindrome(
        private val ignoreCase: Boolean
) : TypeSafeMatcher<String>() {

    override fun describeTo(description: Description) {
        description.appendText("a palindrome string")
    }

    override fun matchesSafely(item: String?): Boolean {
        return !item.isNullOrBlank() && checkPalindrome(item!!)
    }

    private fun checkPalindrome(item: String): Boolean {
        return item.reversed().equals(item, ignoreCase)
    }
}

Wiadomość w przypadku gdy testowany łańcuch znaków nie jest palindromem jest nieco lakoniczna. Możemy ją zmienić poprzez nadpisanie metody describeMismatchSafely(item: String, mismatchDescription: Description). Metoda ta w przeciwieństwie do describeTo posiada referencję do testowanego łańcucha znaków, dzięki czemu możemy wytłumaczyć programiście co jest z nim nie tak:

    override fun describeMismatchSafely(item: String, mismatchDescription: Description) {
        mismatchDescription.appendText("\"$item\" is not equal" +
                " to being reversed : \"${item.reversed()}\"")
    }

Na koniec, należy stworzyć metodę wytwórczą (Factory Method), która stworzy powyższy Matcher. Jako, że matcher posiada parametr ignoreCase, który steruje tym czy sprawdza biorąc pod uwagę wielkość liter czy nie, należy stworzyć 2 metody:

fun isPalindrome() = IsPalindrome(false)
fun isPalindromeIgnoreCase() = IsPalindrome(true)

Dzięki temu nie trzeba przekazywać parametru za każdym razem, gdy użyjesz parametru tylko można użyć bardzo dobrze opisanej metody. Dodatkowo dzięki wykorzystaniu Factory Method, w momencie gdy powyższy Macher nie będzie spełniał wcześniejszych założeń i będziemy chcieli go zaktualizować np. na 2 parametrowy, gdzie drugi parametr będzie sterował tym, że wartość pusta będzie palindromem to będzie można bez przeszkód zaktualizować powyższe metody zamiast aktualizować wszystkich testów:

fun isPalindrome() = IsPalindrome(false, false)
fun isPalindromeIgnoreCase() = IsPalindrome(true, false)
fun isPalindromeWithBlank() = IsPalindrome(false, true)
fun isPalindromeIgnoreCaseWithBlank() = IsPalindrome(true, true)

Matchery, które sprawdzają poprawność kodu powinny być przetestowane, ponieważ jeden mały błąd w Matcherze może mieć negatywne skutki nawet w kilkuset miejscach w testach.

class IsPalindromeTest {

    @Test(expected = AssertionError::class)
    fun nullIsNotPalindrome() {
        assertThat(null, isPalindrome())
    }

    @Test(expected = AssertionError::class)
    fun emptyStringIsNotPalindrome() {
        assertThat("", isPalindrome())
    }

    @Test(expected = AssertionError::class)
    fun blankStringIsNotPalindrome() {
        assertThat("      ", isPalindrome())
    }

    @Test(expected = AssertionError::class)
    fun testNotPalindromeString() {
        assertThat("not palindrome", isPalindrome())
    }

    @Test
    fun testPalindrome() {
        assertThat("a", isPalindrome())
        assertThat("aa", isPalindrome())
        assertThat("aba", isPalindrome())
        assertThat("abcba", isPalindrome())
        assertThat("kajak", isPalindrome())
    }

    @Test(expected = AssertionError::class)
    fun nullIsNotPalindromeIgnoreCase() {
        assertThat(null, isPalindromeIgnoreCase())
    }

    @Test(expected = AssertionError::class)
    fun emptyStringIsNotPalindromeIgnoreCase() {
        assertThat("", isPalindromeIgnoreCase())
    }

    @Test(expected = AssertionError::class)
    fun blankStringIsNotPalindromeIgnoreCase() {
        assertThat("      ", isPalindromeIgnoreCase())
    }

    @Test(expected = AssertionError::class)
    fun testNotPalindromeStringIgnoreCase() {
        assertThat("not palindrome", isPalindromeIgnoreCase())
    }

    @Test
    fun testPalindromeIgnoreCase() {
        assertThat("A", isPalindromeIgnoreCase())
        assertThat("aA", isPalindromeIgnoreCase())
        assertThat("aba", isPalindromeIgnoreCase())
        assertThat("AbCba", isPalindromeIgnoreCase())
        assertThat("kAjaK", isPalindromeIgnoreCase())
    }
}

Linki

Kod źródłowy testów z tego artykułu
Kod źródłowy asercji JUnit
Testy JUnit do asercji
Dokumentacja asercji JUnit
Kod źródłowy Hamcrest
Testy do matcherów w Hamcrest
Dkumentacja Hamcrest

Książki

Czysty kod. Podręcznik dobrego programisty – zobacz
TDD. Sztuka tworzenia dobrego kodu – zobacz

Jeśli uważasz treść za wartościową to podziel się nią z innymi. Dziękuję.

Mateusz Chrustny

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *