JUnit 5 – Fabryki testów

Czasami samo przekazanie parametrów do testów nie wystarcza. Nie są to sytuacje częste, ale mogą się zdarzyć. Jednym z przykładów niech będzie konieczność pobrania danych testowych z jakiejś bazy danych. Nie posiadamy do dyspozycji żadnego narzędzia, w rodzaju @CsvFileSource. Można takie narzędzie napisać. Dodać rozszerzenie do modułu testów parametryzowanych i będzie OK. Ma ono jednak pewne cechy, które parafrazując Twierdzenie GödlaW, można pisać w następujący sposób:

Mechanizm będzie prosty w użyciu np. @DataBaseSource(DbArgumentsProvider.class), ale wymagający wielu dodatkowych elementów do implementacji, albo będzie trudny w użyciu, ale nie będzie wymagać dodatkowych elementów do implementacji np. @DataBaseSource(„DataSourceName”, „QUERY”, ResultSetMapper.class).

I tak źle i tak niedobrze. W dodatku pytanie, co w przypadku innych źródeł danych (JSON, yaml, Excel, XML)? Jak zachowają się testy, gdy zmienimy bazę danych, co w przypadku gdy zapytanie zwraca wyniki w losowej kolejności? Rozwiązaniem są fabryki testów. Mechanizm ten jest obecny w TestNG:

Listing 1. @Factory w TestNG

public class FizzBuzzTestNGFactoryTest {

	@Factory
	public Object[] fizzBuzzTestFactory() {
		return new Object[]{new DividedBy3(),new DividedBy5(), new DividedBy15(), new NotDividedBy3Or5()};
	}

	static class NotDividedBy3Or5 {

		private FizzBuzz sut;

		@BeforeTest
		public void setup() {
			sut = new FizzBuzz();
		}

		@Test
		public void shouldReturnVal() throws Exception {
			assertEquals("2", sut.fizzBuzz(2));
		}
	}

	static class DividedBy5 {

		private FizzBuzz sut;

		@BeforeTest
		public void setup() {
			sut = new FizzBuzz();
		}

		@Test
		public void shouldReturnBuzzIfDiv5() throws Exception {
			assertEquals("Buzz", sut.fizzBuzz(5));
		}
	}

	static class DividedBy3 {
		private FizzBuzz sut;

		@BeforeTest
		public void setup() {
			sut = new FizzBuzz();
		}

		@Test
		public void shouldReturnFizzIfDiv3() throws Exception {
			assertEquals("Fizz", sut.fizzBuzz(3));
		}
	}

	static class DividedBy15 {

		private FizzBuzz sut;

		@BeforeTest
		public void setup() {
			sut = new FizzBuzz();
		}

		@Test
		public void shouldReturnFizzBuzzIfDiv3And5() throws Exception {
			assertEquals("FizzBuzz", sut.fizzBuzz(15));
		}
	}

}

Metoda oznaczona @Factory zwraca tablicę obiektów, które są testami. W tym konkretnym przypadku wykonujemy pracę, którą normalnie robi za nas biblioteka. Jednak można łatwo wyobrazić sobie sytuację, w której proces tworzenia testu jest znacznie bardziej skomplikowany. Chociażby w wyniku wykorzystania @DataProvider jako dostawcy dla fabryki. Powyższy kod jest oczywiście uproszczony. Fabryki przede wszystkim służą do tworzenia testów, gdzie trzeba użyć konstruktora z parametrami:

Listing 2. Klasy testowe z parametrami w konstruktorach w TestNG

public class FizzBuzzTestNGFactoryConstructorCallTest {

	@Factory
	public Object[] fizzBuzzTestFactory() {
		FizzBuzz sut = new FizzBuzz();
		return Stream.of(
				IntStream.of(3, 6, 99).<Object>mapToObj(p -> new DividedBy3(sut, p)),
				IntStream.of(5, 10, 50).<Object>mapToObj(p -> new DividedBy5(sut, p)),
				IntStream.of(15, 30, 150).<Object>mapToObj(p -> new DividedBy15(sut, p)),
				IntStream.of(2, 8, 11).<Object>mapToObj(p -> new NotDividedBy3Or5(sut, p))
		).flatMap(Function.identity())
				.collect(Collectors.toSet()).toArray();
	}

	static class NotDividedBy3Or5 {

		private final FizzBuzz sut;
		private final int param;

		public NotDividedBy3Or5(FizzBuzz sut, int param) {
			this.sut = sut;
			this.param = param;
		}

		@Test
		public void shouldReturnVal() throws Exception {
			assertEquals(param + "", sut.fizzBuzz(param));
		}
	}

	static class DividedBy5 {


		private final FizzBuzz sut;
		private final int param;

		DividedBy5(FizzBuzz sut, int param) {
			this.sut = sut;
			this.param = param;
		}

		@Test
		public void shouldReturnBuzzIfDiv5() throws Exception {
			assertEquals("Buzz", sut.fizzBuzz(param));
		}
	}

	static class DividedBy3 {
		private final FizzBuzz sut;
		private final int param;

		DividedBy3(FizzBuzz sut, int param) {
			this.sut = sut;
			this.param = param;
		}

		@Test
		public void shouldReturnFizzIfDiv3() throws Exception {
			assertEquals("Fizz", sut.fizzBuzz(param));
		}
	}

	static class DividedBy15 {

		private final FizzBuzz sut;
		private final int param;

		DividedBy15(FizzBuzz sut, int param) {
			this.sut = sut;
			this.param = param;
		}

		@Test
		public void shouldReturnFizzBuzzIfDiv3And5() throws Exception {
			assertEquals("FizzBuzz", sut.fizzBuzz(param));
		}
	}

}

W dodatku rzeczywisty kod może być rozbity na wiele klas w osobnych plikach. To są jednak szczegóły. W JUnit 4 nie było możliwości zrobienia czegoś takiego. JUnit 5 wprowadza mechanizm testów dynamicznych.

Testy dynamiczne

Testy dynamiczne, to testy, które są tworzone w czasie wykonania programu. W połączeniu z mechanizmem fabryk pozwalają one na wygenerowanie testów w locie. Oczywiście coś za coś:

Listing 3. Testy dynamiczne w JUnit 5

public class FizzBuzzJUnit5DynamicTest {

	private FizzBuzz sut = new FizzBuzz();

	@Nested
	class DividedBy15 {

		@TestFactory
		public Collection<DynamicTest> shouldReturnFizzBuzzIfDiv3And5() throws Exception {
			return Arrays.asList(
					dynamicTest("For 15", () -> assertEquals("FizzBuzz", sut.fizzBuzz(15))),
					dynamicTest("For 30", () -> assertEquals("FizzBuzz", sut.fizzBuzz(30))),
					dynamicTest("For 150", () -> assertEquals("FizzBuzz", sut.fizzBuzz(150)))
			);
		}
	}


	@Nested
	class DividedBy5 {

		@TestFactory
		public Stream<DynamicTest> shouldReturnBuzzIfDiv5() throws Exception {
			return Stream.of(5, 10, 50)
					.map(
							val -> dynamicTest(String.format("For %s", val)
									, () -> assertEquals("Buzz", sut.fizzBuzz(val))
							)
					);
		}
	}


	@Nested
	class DividedBy3 {

		@TestFactory
		public Iterable<DynamicTest> shouldReturnFizzIfDiv3() throws Exception {
			return Arrays.asList(
					dynamicTest("for 3", () -> assertEquals("Fizz", sut.fizzBuzz(3))),
					dynamicTest("for 6", () -> assertEquals("Fizz", sut.fizzBuzz(6))),
					dynamicTest("for 99", () -> assertEquals("Fizz", sut.fizzBuzz(99)))
			);
		}
	}

	@Nested
	class NotDividedBy3Or5 {

		@TestFactory
		public Iterator<DynamicTest> shouldReturnVal() throws Exception {
			return Arrays.asList(
					dynamicTest("for 2", () -> assertEquals("2", sut.fizzBuzz(2))),
					dynamicTest("for 9", () -> assertEquals("8", sut.fizzBuzz(8))),
					dynamicTest("for 11", () -> assertEquals("11", sut.fizzBuzz(11)))
			).iterator();
		}
	}
}

Uwaga, używam mechanizmu testów zagnieżdżonych, by wszystko było w jednym miejscu.

Z metody oznaczonej jako @TestFactory, zwracamy kolekcję, iterator, strumień, etc. obiektów DynamicTest. Obiekty te reprezentują poszczególne testy. Wygląda to nieźle, biorąc pod uwagę, że możemy w łatwy sposób np. mapować ResultSet na Stream. To, co odróżnia testy dynamiczne od zwykłych testów, jest inny cykl życia. W przypadku testów dynamicznych nie są uruchamiane metody oznaczone adnotacjami @BeforeEach i @AfterEach. Jest tak, ponieważ klasa DynamicTest, jest tylko wrapperem na implementację Executable, które reprezentuje test jako taki. Executable, jest interfejsem oznaczonym jako @FunctionalInterface. Poza ograniczeniem do pojedynczej metody, nie ma możliwości przekazania zmiennych pomiędzy poszczególnymi lambdami, bez łamania kontraktu.

Jeżeli chcemy użyć @BeforeEach i @AfterEach, to możemy zrobić to na poziomie klasy, w której mamy zdefiniowaną metodę fabrykującą:

Listing 4. Użycie @BeforeEach i @AfterEach z testami dynamicznymi

public class FizzBuzzJUnit5DynamicBeforeTest {

	@Nested
	class DividedBy15 {

		private FizzBuzz sut;

		@BeforeEach
		public void setUp() {
			sut = new FizzBuzz();
		}

		@TestFactory
		public Collection<DynamicTest> shouldReturnFizzBuzzIfDiv3And5() throws Exception {
			return Arrays.asList(
					dynamicTest("For 15", () -> assertEquals("FizzBuzz", sut.fizzBuzz(15))),
					dynamicTest("For 30", () -> assertEquals("FizzBuzz", sut.fizzBuzz(30))),
					dynamicTest("For 150", () -> assertEquals("FizzBuzz", sut.fizzBuzz(150)))
			);
		}
	}


	@Nested
	class DividedBy5 {
		private FizzBuzz sut;

		@BeforeEach
		public void setUp() {
			sut = new FizzBuzz();
		}

		@TestFactory
		public Stream<DynamicTest> shouldReturnBuzzIfDiv5() throws Exception {
			return Stream.of(5, 10, 50)
					.map(
							val -> dynamicTest(String.format("For %s", val)
									, () -> assertEquals("Buzz", sut.fizzBuzz(val))
							)
					);
		}
	}


	@Nested
	class DividedBy3 {
		private FizzBuzz sut;

		@BeforeEach
		public void setUp() {
			sut = new FizzBuzz();
		}

		@TestFactory
		public Iterable<DynamicTest> shouldReturnFizzIfDiv3() throws Exception {
			return Arrays.asList(
					dynamicTest("for 3", () -> assertEquals("Fizz", sut.fizzBuzz(3))),
					dynamicTest("for 6", () -> assertEquals("Fizz", sut.fizzBuzz(6))),
					dynamicTest("for 99", () -> assertEquals("Fizz", sut.fizzBuzz(99)))
			);
		}
	}

	@Nested
	class NotDividedBy3Or5 {
		private FizzBuzz sut;

		@BeforeEach
		public void setUp() {
			sut = new FizzBuzz();
		}

		@TestFactory
		public Iterator<DynamicTest> shouldReturnVal() throws Exception {
			return Arrays.asList(
					dynamicTest("for 2", () -> assertEquals("2", sut.fizzBuzz(2))),
					dynamicTest("for 9", () -> assertEquals("8", sut.fizzBuzz(8))),
					dynamicTest("for 11", () -> assertEquals("11", sut.fizzBuzz(11)))
			).iterator();
		}
	}
}

Przy czym metody te zostaną wywołane raz, przed wywołaniem metody fabrykującej.

Podsumowanie

Mechanizm testów dynamicznych w JUnit 5 działa inaczej niż fabryki z TestNG. Ma on pewne ograniczenia, ale nie są one na tyle duże, by całkowicie przekreślić ten mechanizm.

Napisz odpowiedź

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax