Pułapka assume
Dawno, dawno temu, jak pojawił się JUnit5, to popełniłem cykl tekstów o tym narzędziu. W jednym z
nich opisałem czym są założenia, Assumptions
. Obecnie pracując z Quarkusem i mavenem naciąłem się na
bardzo ciekawy „błąd”. Nie jest to oczywiście błąd-błąd, ale może prowadzić do dość ciekawych „wyników” w czasie testów.
Czym są założenia – w skrócie
Założenia, to mechanizm podobny do asercji, ale przeznaczony do weryfikowania stanu PRZED testem. Dzięki temu możemy wychwycić nieprawidłowy stan, który wpływa na testy np. niepoprawny stan testowanego systemu, który pozostał po poprzednich testach.
Bywa to szczególnie przydane, gdy pracujemy z testami na pograniczu jednostkowych i integracyjnych. Możemy na przykład zweryfikować, czy baza danych nie została zanieczyszczona przez inne testy. W ogólności nieudane sprawdzenie założenia powinno skutkować przerwaniem testu i oznaczeniem go jako czerwonego.
Jak to działa w Mavenie?
Ano nie tak jak się spodziewamy. Mamy taki oto serwis
Listing 1. Przykładowy serwis
@ApplicationScoped
class SimpleService {
private final AtomicInteger counter = new AtomicInteger(0);
public int inc() {
return counter.incrementAndGet();
}
public int get() {
return counter.get();
}
}
Oraz dwa testy, które współdzielą stan, co jest oczywiście niepożądane:
Listing 2. Przykładowy test
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.Assumptions;
import org.junit.jupiter.api.Test;
@QuarkusTest
class SimpleServiceTest {
@Inject
SimpleService simpleService;
@Test
void test1() {
Assumptions.assumeThat(simpleService.get()).isEqualTo(0);
Assertions.assertThat(simpleService.inc()).isEqualTo(1);
}
@Test
void test2() {
Assumptions.assumeThat(simpleService.get()).isEqualTo(0);
Assertions.assertThat(simpleService.inc()).isEqualTo(1);
}
}
Jeżeli test ten uruchomimy w IDE, to oczywiście jeden nie zostanie wykonany. Dostaniemy błąd:
Listing 3. I jego wynik
org.opentest4j.TestAbortedException:
assumption was not met due to:
expected:0
but was:1
at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502)
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:486)
at org.assertj.core.api.AssumptionExceptionFactory.buildAssumptionException(AssumptionExceptionFactory.java:49)
at org.assertj.core.api.AssumptionExceptionFactory.assumptionNotMet(AssumptionExceptionFactory.java:32)
at org.assertj.core.api.Assumptions$AssumptionMethodInterceptor.intercept(Assumptions.java:126)
at org.assertj.core.api.IntegerAssert$ByteBuddy$Uaxa0C94.isEqualTo(Unknown Source)
at org.assertj.core.api.IntegerAssert$ByteBuddy$Uaxa0C94.isEqualTo(Unknown Source)
at org.acme.SimpleServiceTest.test2(SimpleServiceTest.java:23)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at io.quarkus.test.junit.QuarkusTestExtension.runExtensionMethod(QuarkusTestExtension.java:973)
at io.quarkus.test.junit.QuarkusTestExtension.interceptTestMethod(QuarkusTestExtension.java:823)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1597)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1597)
Caused by:org.opentest4j.AssertionFailedError:
expected:0
but was:1
at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502)
...6more
2024-08-06 14:28:32,386INFO [io.quarkus](main)q-assume stopped in 0.004s
Process finished with exit code 255
A teraz po uruchomieniu z linii poleceń:
Listing 4. I jego wynik
$ mvn clean verify
[INFO] Scanning for projects...
[INFO]
[INFO] -------------------------< org.acme:q-assume >--------------------------
[INFO] Building q-assume 1.0.0-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- clean:3.2.0:clean (default-clean) @ q-assume ---
[INFO] Deleting /home/koziolek/workspace/blog/blog/q-assume/target
[INFO]
[INFO] --- resources:3.3.1:resources (default-resources) @ q-assume ---
[INFO] Copying 1 resource from src/main/resources to target/classes
[INFO]
[INFO] --- quarkus:3.13.0:generate-code (default) @ q-assume ---
[INFO]
[INFO] --- compiler:3.13.0:compile (default-compile) @ q-assume ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 2 source files with javac [debug release 21] to target/classes
[INFO]
[INFO] --- quarkus:3.13.0:generate-code-tests (default) @ q-assume ---
[INFO]
[INFO] --- resources:3.3.1:testResources (default-testResources) @ q-assume ---
[INFO] skip non existing resourceDirectory /home/koziolek/workspace/blog/blog/q-assume/src/test/resources
[INFO]
[INFO] --- compiler:3.13.0:testCompile (default-testCompile) @ q-assume ---
[INFO] Recompiling the module because of changed dependency.
[INFO] Compiling 2 source files with javac [debug release 21] to target/test-classes
[INFO]
[INFO] --- surefire:3.2.5:test (default-test) @ q-assume ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running org.acme.SimpleServiceTest
2024-08-06 14:25:15,238 INFO [io.quarkus] (main) q-assume 1.0.0-SNAPSHOT on JVM (powered by Quarkus 3.13.0) started in 0.953s.
2024-08-06 14:25:15,241 INFO [io.quarkus] (main) Profile test activated.
2024-08-06 14:25:15,241 INFO [io.quarkus] (main) Installed features: [cdi, picocli]
[WARNING] Tests run: 2, Failures: 0, Errors: 0, Skipped: 1, Time elapsed: 2.137 s -- in org.acme.SimpleServiceTest
2024-08-06 14:25:15,314 INFO [io.quarkus] (main) q-assume stopped in 0.005s
[INFO]
[INFO] Results:
[INFO]
[WARNING] Tests run: 2, Failures: 0, Errors: 0, Skipped: 1
[INFO]
[INFO]
[INFO] --- jar:3.3.0:jar (default-jar) @ q-assume ---
[INFO] Building jar: /home/koziolek/workspace/blog/blog/q-assume/target/q-assume-1.0.0-SNAPSHOT.jar
[INFO]
[INFO] --- quarkus:3.13.0:build (default) @ q-assume ---
[INFO] [io.quarkus.deployment.QuarkusAugmentor] Quarkus augmentation completed in 678ms
[INFO]
[INFO] --- failsafe:3.2.5:integration-test (default) @ q-assume ---
[INFO] Tests are skipped.
[INFO]
[INFO] --- failsafe:3.2.5:verify (default) @ q-assume ---
[INFO] Tests are skipped.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 5.969 s
[INFO] Finished at: 2024-08-06T14:25:16+02:00
[INFO] ------------------------------------------------------------------------
Ups… zielono mi :D
Założenia nie działają?
No nie do końca. Założenia działają trochę inaczej niż intuicyjny opis z początku tekstu. Z dokumentacji:
In direct contrast to failed assertions, failed assumptions do not result in a test failure; rather, a failed assumption results in a test being aborted.
Czyli zachowanie w mavenie jest OK. IDE nie do końca jest OK, choć czerwony stacktrace po wykonaniu testu odpowiednio podnosi ciśnienie kawy w krwii i pozwala na zauważenie problemu. Zielone testy zazwyczaj ignorujemy. W końcu są zielone.
Jak się przed tym bronić?
Na krótszą metę można spróbować uruchamiać test i śledzić czy pokrycie rośnie. Na dłuższą należy przyjąć, że wszystkie testy powinny się wykonać i za
pomocą odpowiednich reguł ArchUnita ograniczyć czy to użycie @Disable
, czy to Assumptions
. Jeżeli chcesz sprawdzić środowisko przed uruchomieniem
testu, to użyj asercji. Może nie będzie to zgrabne, ale będzie skuteczne.
Podsumowanie
Dlaczego o tym piszę? Jak wspomniałem na początku, ostatnio dużo pracuję z Quarkusem. Zmienił się mój model pracy. Dotychczas testy puszczałem bezpośrednio z IDE, a od pewnego czasu przerzuciłem się na automatyczne śledzenie zmian i uruchamianie testów z quarkusowego pluginu. Przy dużej ilości testów jeden czy dwa, które nie zostały wykonane, mogą umknąć uwadze i mamy problem. Zatem jest to wpis z serii „uczmy się na błędach, najlepiej na cudzych”.