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
doassertThat
Czym jest assert?
W kotlinie jest to funkcja assert(value : Boolean)
(w Javie słowo kluczowe
), która w przypadku otrzymania wartości false rzuca wyjątek assert
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.
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
. Test równości jest wykonywany za pomocą metody operand
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
z uwzględnieniem błędu operand
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