Fajne testowanie

JUnit 5 vs. Spock​

Perspektywa... ma znaczenie :)

Narzędzia

  • Runner
    • Sposób na powtarzalne odpalanie testów
  • Framework
    • Ramy na testy, generowanie raportu itp.
  • Asercje
    • Porównywanie rezultatów z wartościami oczekiwanymi
  • Mockowanie
    • Symulowanie zewnętrznych zależności

JUnit


                        class ScheduleTest {
                            void scheduleOnCall_throwsWhenDateAlreadyTaken() {
                            }
                        
                            void scheduleOnCall_throwsWhenRoomAlreadyTaken() {
                            }
                        
                            void scheduleOnCall_worksAsExpected() {
                            }
                        
                            void scheduleNewVisit_throwsWhenDoctorUnavailable() {
                            }
                        
                            void scheduleNewVisit_throwsWhenDateAlreadyTaken() {
                            }
                        
                            void scheduleNewVisit_worksAsExpected() {
                            }
                        
                            void overrideOnCall_throwsWhenDifferentSpecialization() {
                            }
                        
                            void overrideOnCall_worksAsExpected() {
                            }
                        }
                    

JUnit 5


                        class ScheduleTest {
                            @Test
                            @DisplayName("should throw when scheduling for the taken date")
                            void scheduleOnCall_throwsWhenDateAlreadyTaken() {
                            }
                        
                            @Test
                            @DisplayName("should throw when scheduling with the taken room")
                            void scheduleOnCall_throwsWhenRoomAlreadyTaken() {
                            }
                        
                            @Test
                            @DisplayName("should schedule a new on call date")
                            void scheduleOnCall_worksAsExpected() {
                            }
                        
                            @Test
                            @DisplayName("should throw when scheduling a visit to an absent doctor")
                            void scheduleNewVisit_throwsWhenDoctorUnavailable() {
                            }
                        
                            @Test
                            @DisplayName("should throw when scheduling a visit for the taken date")
                            void scheduleNewVisit_throwsWhenDateAlreadyTaken() {
                            }
                        
                            @Test
                            @DisplayName("should schedule a new visit")
                            void scheduleNewVisit_worksAsExpected() {
                            }
                        
                            @Test
                            @DisplayName("should throw when overriding on call with a doctor with a different specialization")
                            void overrideOnCall_throwsWhenDifferentSpecialization() {
                            }
                        
                            @Test
                            @DisplayName("should override on call")
                            void overrideOnCall_worksAsExpected() {
                            }
                        }
                    

JUnit 5

JUnit Platform + JUnit Jupiter + JUnit Vintage


                        
                            
                            
                                org.spockframework
                                spock-core
                                1.3-groovy-2.5
                                test
                            
                            
                                org.codehaus.groovy
                                groovy-all
                                2.5.9
                                pom
                                test
                            
                        
                        
                            
                                
                                
                                    org.codehaus.gmavenplus
                                    gmavenplus-plugin
                                    1.8.1
                                    
                                        
                                            
                                                addTestSources
                                                compileTests
                                            
                                        
                                    
                                
                                
                                    org.apache.maven.plugins
                                    maven-surefire-plugin
                                    
                                        --illegal-access=permit
                                    
                                
                            
                        
                    

                        plugins {
                            id 'org.springframework.boot' version '2.2.5.RELEASE'
                            id 'io.spring.dependency-management' version '1.0.9.RELEASE'
                            id 'java'
                            id 'groovy'
                        }
                        
                        ext['groovy.version'] = '3.0.1'
                        
                        /* ... */
                        
                        repositories {
                            mavenCentral()
                        }
                        
                        dependencies {
                            implementation 'org.springframework.boot:spring-boot-starter'
                            testImplementation('org.springframework.boot:spring-boot-starter-test') {
                                exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
                            }
                            testImplementation 'org.codehaus.groovy:groovy-all:3.0.1'
                            testImplementation 'org.spockframework:spock-core:2.0-M2-groovy-3.0'
                        }
                        
                        test {
                            useJUnitPlatform()
                        }
                    

                        import spock.lang.Specification

                        class ScheduleSpec extends Specification {
                            def 'should throw when scheduling for the taken date'() {
                            }

                            def 'should throw when scheduling with the taken room'() {
                            }

                            def 'should schedule a new on call date'() {
                            }

                            def 'should throw when scheduling a visit to an absent doctor'() {
                            }

                            def 'should throw when scheduling a visit for the taken date'() {
                            }

                            def 'should schedule a new visit'() {
                            }

                            def 'should throw when overriding on call with a doctor with a different specialization'() {
                            }

                            def 'should override on call'() {
                            }
                        }
                    

                        class ScheduleSpec extends Specification {
                            def 'should throw when scheduling for the taken date'() {
                                expect:
                                true
                            }
                        
                            def 'should throw when scheduling with the taken room'() {
                                expect:
                                true
                            }
                        
                            def 'should schedule a new on call date'() {
                                expect:
                                true
                            }
                        
                            def 'should throw when scheduling a visit to an absent doctor'() {
                                expect:
                                true
                            }
                        
                            def 'should throw when scheduling a visit for the taken date'() {
                                expect:
                                true
                            }
                        
                            def 'should schedule a new visit'() {
                                expect:
                                true
                            }
                        
                            def 'should throw when overriding on call with a doctor with a different specialization'() {
                                expect:
                                true
                            }
                        
                            def 'should override on call'() {
                                expect:
                                true
                            }
                        }
                    

Groovy - idealny do DSL


                        def end = start + Duration.of(2, HOURS) // method "plus"

                    toTest.getPatient() == null // equals

                    entry.doctor // getDoctor()

                    [roomFoo, new Room('bar')] // new ArrayList()

                    factory() // lambda execution

                    toTest.snapshot.entries.sort { it.from } // collection processing

                    result[0] // result.get(0)

                    
                    private static ScheduleEntry exampleVisit( // default params
                        ZonedDateTime from = start, 
                        ZonedDateTime to = end
                    ) { /* ... */ }

                    
                    private static Doctor exampleSurgeon() {
                        new Doctor(Specialization.SURGEON) // look ma, no return
                    }thrown DateAlreadyTakenException
                    
                    def 'should not execute getter'() {
                        given:
                        def room = GroovyMock(Room)
                
                        when:
                        new Schedule([room])
                
                        then:
                        0 * room.name
                    }

                            private Schedule toTest;

                            @BeforeEach
                            void init() {
                                toTest = new Schedule();
                            }

                            @Test
                            @DisplayName("should throw when scheduling for the taken date")
                            void scheduleOnCall_throwsWhenDateAlreadyTaken() {
                                // given
                                Doctor first = exampleSurgeon();
                                var start = ZonedDateTime.now();
                                var end = start.plusHours(2);
                                // and
                                toTest.scheduleOnCall(first, start, end);

                                // when
                                Doctor second = exampleSurgeon();
                                var start2 = start.plusHours(1);

                                // then
                                assertThrows(
                                    DateAlreadyTakenException.class, 
                                    () -> toTest.scheduleOnCall(second, start2, end)
                                );
                            }

                            private static Doctor exampleSurgeon() {
                                return new Doctor(Specialization.SURGEON);
                            }
                        

                            @Subject
                            private Schedule toTest


                            def setup() {
                                toTest = new Schedule()
                            }


                            def 'should throw when scheduling for the taken date'() {
                                given:
                                Doctor first = exampleSurgeon()
                                def start = ZonedDateTime.now()
                                def end = start.plusHours(2)
                                and:
                                toTest.scheduleOnCall(first, start, end)
                                
                                when:
                                Doctor second = exampleSurgeon()
                                def start2 = start.plusHours(1)
                                and:
                                toTest.scheduleOnCall(second, start2, end)
                                
                                then:
                                thrown DateAlreadyTakenException
                            }


                            private static Doctor exampleSurgeon() {
                                new Doctor(Specialization.SURGEON)
                            }
                        

Parametryzowanie


                        @ParameterizedTest(name = "{index}: {0}")
                        @MethodSource
                        @DisplayName("should interfere when start between an existing start and end")
                        void interferesWith_otherWhichStartsBeforeThisEnds(ZonedDateTime startBetween) {
                            // given
                            ZonedDateTime lateEnd = startBetween.plusHours(2);

                            // expect
                            assertTrue(entry.interferesWith(exampleEntry(startBetween, lateEnd)));
                        }

                        static Stream<String> interferesWith_otherWhichStartsBeforeThisEnds() {
                            return Stream.of(
                                    start.plusSeconds(1).toString(),
                                    start.plusHours(1).toString(),
                                    end.minusSeconds(1).toString()
                            );
                        }
                    
  • @CsvSource, @CsvFileSource
  • @ValueSource, np.
    
                                    @ValueSource(ints = { 1, 2, 3 })
                                
  • @EnumSource, np.
    
                                    @EnumSource(ChronoUnit.class)
                                
  • @ArgumentsSource - reużywanie Arguments
  • @NullAndEmptySource + null + empty

                        @Unroll
                        def 'should interfere when start between an existing start and end'() {
                            given:
                            def lateEnd = startBetween + of(2, HOURS)

                            expect:
                            entry.interferesWith(exampleEntry(startBetween, lateEnd))

                            where:
                            startBetween << [
                                    start + of(1, SECONDS),
                                    start + of(30, MINUTES),
                                    end - of(1, SECONDS)
                            ]
                        }
                    

                        @Unroll
                        def 'should interfere when end between an existing start and end (#description)'() {
                            given:
                            def newStart = endBetween - of(2, HOURS)

                            expect:
                            entry.interferesWith(exampleEntry(newStart, endBetween))

                            where:
                            endBetween             | description
                            end - of(1, SECONDS)   | 'end 1s earlier'
                            start + of(1, SECONDS) | 'end 1s after start'
                            start + of(1, HOURS)   | 'end in between'
                        }
                    

Dynamika


                        @TestFactory
                        @DisplayName("is identified by its values")
                        Stream<DynamicNode> generateEqualityTests() {
                            return Stream.<Supplier<ScheduleEntry>>of(
                                    () -> new ScheduleEntry(
                                            new Doctor(Specialization.SURGEON),
                                            start,
                                            end,
                                            new Room("example"),
                                            new Patient("Frank")
                                    ), () -> new ScheduleEntry(
                                            new Doctor(Specialization.SURGEON),
                                            start,
                                            end,
                                            new Room("example")
                                    )
                            ).map(factory ->
                                    dynamicTest(
                                            factory.get().getPatient() != null ? "with patient" : "without patient",
                                            () -> assertEquals(factory.get(), factory.get())
                                    )
                            );
                        }
                    

Test DSL


                        @TestFactory
                        @DisplayName("calculateSquare")
                        Stream<DynamicNode> dynamicTestsWithContainers() {
                            Function<Integer, Long> testExecution = (input) -> toTest.calculateSquare(input);

                            return Stream.of(
                                    new TestSuite("positive numbers", new TestCase[]{
                                            new TestCase(3, 9L),
                                            new TestCase(100, 10_000L)
                                    }),
                                    new TestSuite("negative numbers", new TestCase[]{
                                            new TestCase(-3, 9L)
                                    }),
                                    new TestSuite("specific numbers", new TestCase[]{
                                            new TestCase(1, 1L),
                                            new TestCase(0, 0L)
                                    }),
                                    new TestSuite("big numbers", new TestCase[]{
                                            new TestCase(Integer.MAX_VALUE, 4_611_686_014_132_420_609L),
                                            new TestCase(Integer.MIN_VALUE, 4_611_686_018_427_387_904L)
                                    })
                            ).map(testSuite -> testSuite.toDynamicContainer(testExecution));
                        }
                    

Spock? where!


                        @Unroll
                        def 'is identified by its values (#description)'() {
                            expect:
                            factory() == factory()

                            where:
                            factory << [
                                    () -> new ScheduleEntry(
                                            new Doctor(Specialization.SURGEON),
                                            start,
                                            end,
                                            new Room('example'),
                                            new Patient('Frank')
                                    ),
                                    () -> new ScheduleEntry(
                                            new Doctor(Specialization.SURGEON),
                                            start,
                                            end,
                                            new Room('example')
                                    )
                            ]
                            description << ['with patient', 'without patient']
                        }
                    

Grupowanie

  • @RepeatedTest(10)
  • @Nested
  • @Tag, np.
    
                                    @Tag("unit")
                                    class ScheduleTest {
                                
    
                                    test {
                                        useJUnitPlatform {
                                            includeTags 'unit'
                                        }
                                    }
                                

Asercje

var e =     assertThrows(
            RoomAlreadyTakenException.class,
            () -> toTest.scheduleOnCall(new ScheduleEntry(
                    exampleSurgeon(),
                    start.plusHours(1),
                    start.plusHours(3),
                    roomFoo
            ))
    );assertEquals("Cannot schedule for a room \"foo\"", e.getMessage());

assertEquals(actual, expected)
czy
assertEquals(expected, actual)
?


                            then:
                            def e = thrown RoomAlreadyTakenException
                            e.message.contains(roomFoo.name)
                        

Agregowanie


                        @Test
                        @DisplayName("should schedule a new on call date")
                        void scheduleOnCall_worksAsExpected() {
                          assertAll(
                            // first schedule = works as a charm!
                            () -> assertDoesNotThrow(() -> toTest.scheduleOnCall(exampleEntry(start, end))),
                            // second time? NO WAY, already scheduled
                            () -> assertThrows(
                              BusinessScheduleException.class,
                              () -> toTest.scheduleOnCall(exampleEntry(start, end))
                            )
                          );
                        }
                    

(Moje) podsumowanie

Moc IDE API
JUnit 🤩 🤩 🙂
Spock 🤩 🤨 🤩

A co z TestNG?


                    @Test(
                        dataProvider = "configuredProprietaryAndNotProprietaryDataSources", 
                        testName = "should pass when valid license",
                        groups = "unit"
                    )
                    public void shouldPassWhenValidLicenseAndProprietaryDataSources(
                        List<CatalogType> proprietaryCatalogTypes,
                        List<CatalogType> otherCatalogTypes
                    ) {
                        // ...
                    }

                    @DataProvider
                    public Object[][] configuredProprietaryAndNotProprietaryDataSources() {
                        return new Object[][] {
                            { asList(ORACLE, TERADATA), asList(KAFKA, MYSQL, MONGODB) },
                            { singletonList(ORACLE), asList(MYSQL, KAFKA, S3) },
                            { singletonList(TERADATA), asList(S3, MYSQL) },
                            { singletonList(SNOWFLAKE), asList(S3, MYSQL) }
                        };
                    }
                

Dzięki!