Spock i ArgumentCaptor

1

Próbuję napisać test z użyciem spocka. Moja klasa która musi zostać przetestowana:

@AllArgsConstructor
public class Client {
    private Parser parser;
    
    public List<String> getParsedText() {
        return parser.parse("foo");
    }
}

Test napisany przy użyciu JUnit i Mockito:

@RunWith(MockitoJUnitRunner.class)
public class ClientTest {
    @InjectMocks
    private Client client;
    @Mock
    private Parser parser;
   
    @Test
    public void shouldReturnParsedText() {
        // given
        List<String> expectedResult = Collections.singletonList("result");
        when(parser.parse(eq("foo"))
            .thenReturn(expectedResult);

        // when
        List<String> result = client.getParsedText();

        // then
        assertEquals(result, expectedResult);
        verify(parser, times(1)).parse(eq("foo"));
    }
}

Ten sam test napisany przy użyciu Spock framework:

public class ClientSpec extends Specification {
    def parser = Mock(Parser)
    def client = new Client(parser)

    def "should return parsed text"() {
        given:
        def expectedResult = ["result"];
        parser.parse("foo") >> expectedResult;

        when:
        def result = client.getParsedText();

        then:
        result == expectedResult
        1 * client.parse({
            it == "foo"
        })
}

Metoda parse() wykonana przez klasę Client zwraca null. Jeżeli napiszę kod jak poniżej, działa.

public class ClientSpec extends Specification {
    def parser = Mock(Parser)
    def client = new Client(parser)

    def "should return parsed text"() {
        given:
        def expectedResult = ["result"];

        when:
        def result = client.getParsedText();

        then:
        result == expectedResult
        1 * client.parse({
            it == "foo"
        }) >> expectedResult
}

Co ciekawe w tym przypadku then i clousure przekazane do parse wykonuje się przed wykonaniem faktycznego kodu. Dodatkowy problem z tym jest taki że mockujemy zachowanie metod w sekcji then, co jest niepoprawne.

Co może być przyczyną takiego zachowania spocka? Jak to naprawić. Nie będę ukrywał że testy napisane w spocku są o wiele ładniejsze i czytelniejsze.

0

Probowales

parser.parse("foo") >> expectedResult

Dac w fazie "and"?

0

Tak, niestety bezskutecznie.
Inne pomysły? :)

0

Mój problem opisany jest pod drugim z linków, które podesłałeś w sekcji "Verifying interaction of a method while changing its behaviour".
Tutaj link
Autor zastosował moim zdaniem średnie rozwiązanie, ale czy jest lepsze?

0

A nie potrzebujesz odpowiednika InjectMocks?

0

Nie, ponieważ nie używam dependency injection/mock injection. Robię to ręcznie za pomocą

def client = new Client(parser)
0

A może przedobrzyłeś w ostatniej linii. W dokumentacji robią tak:

then:
2 * subscriber.receive("hello")

Nie ma tu żadnego closure.

0
jarekczek napisał(a):

A może przedobrzyłeś w ostatniej linii. W dokumentacji robią tak:

then:
2 * subscriber.receive("hello")

Nie ma tu żadnego closure.

Jeżeli argumentem metody, zamiast stringa będzie np. tablica i będę chciał sprawdzić czy tablica ma 2 elementy, to potrzebuje clousure

2 * subscriber.receive({ it.size() == 2 })

Podobne do tego przykłady też są w dokumentacji :)
Problem polega na połączeniu mockowania metody i wartości zwracanej przez nią i walidacji czy metoda została wykonana

0

Nie, nie porównuję stringa z GStringiem. Błąd polega na tym ze metoda mockowana zwraca null wewnątrz testowanej klasy.

0

a może @Shalom coś o tym wie? :)

0

Nie korzystam ze Spocka. Nienawiść do grooviego, tak zostałem wychowany #pdk
Poza tym ten kod do przetestowania nie ma sensu i robisz tutaj full whitebox test w którym testujesz w sumie tylko czy mock działa.

0

Spock jest spoko. Lepszy.

Ale test no nie da sie ukryc bez sensu ;) ale to pewnie tylko przyklad.

1

Jednak wpadłeś w pułapkę spocka i jego luźną składnię. Nie ma czegoś takiego jak client.parse, więc nigdy nie zostanie wywołane :)

To co lepsze, Mockito czy Spock?

0

...ale to tylko detal. Próbuję ten Twój test uruchomić i zrozumiałem, że źle myślimy. Zobacz to:

In particular, the following Mockito-style splitting of stubbing and mocking into two separate statements will not work:

I tam jakiś przykład. Krótko mówiąc, nie można bezpośrednio przenosić testów z Mockito. W Spocku jakoś inaczej się myśli. No i oni piszą, że So far, we declared all our interactions in a then: block. (Where to declare interactions). Czyli to, co Ty zauważyłeś.

Myślałem, że to będzie prostsze, a tu taka lekcja. No i ta dokumentacja, niezbyt do mnie przemawia. Dzięki za zabawę, ale mam dość :)

EDIT

Dokumentacja Spocka jednak nie jest zła, tylko trzeba zacząć na spockojnie od Spock Primer. No i Kret wrzucił nas tu od razu na głęboką wodę, bo to może jest najdziwniejszy feature Spocka, o którym nawet dokładnie piszą w dokumentacji. W rozdziałach, które już linkowałem:

Stubbing:

When stubbing a method, you don’t care if and how many times the method is going to be called

Combining Mocking and Stubbing

When mocking and stubbing the same method call, they have to happen in the same interaction.

No i to by była odpowiedź na pytanie Kreta: Co może być przyczyną takiego zachowania spocka? Wnioskuję, że inaczej się nie da, może w przyszłych wersjach. Jak dla mnie to argumenty Kreta są słuszne i wolałbym, żeby pierwszy przykład działał.

1

@jarekczek: Gdzieś w dokumentacji spocka znalazłem informację że taka forma stubowania jest czytelniejsza.
Starając się to zrozumieć napisałem test.
Wygląda on tak:

class UserServiceSpec extends Specification {
    def userRepository = Mock(UserRepository)
    def service = new UserService(userRepository)

    def "registering already registered user should throw exception"() {
        given: "already registered user"
        def user = new User("Adam", "Haslo")

        and: "credentials which has the same username as already registered user"
        def credentials = new CredentialsDto(username: "Adam", password: "asdasd")

        when: "user tries to register"
        service.registerUser(credentials)

        then: "repository should return found user in database once"
        1 * userRepository.findByName(credentials.username) >> Optional.of(user)

        and: "service should throw exception"
        thrown(UserAlreadyRegisteredException)
    }
}

klasa testowana:

@AllArgsConstructor
public class UserService {
    private final UserRepository userRepository;

    public void registerUser(CredentialsDto credentials) {
        User user = userRepository.findByName(credentials.getUsername())
                .orElseThrow(() -> new UserAlreadyRegisteredException());
        // ...other logic
    }
}

Uważałem że stubowanie metody nie powinno być w sekcji then, ale widząc taki test, który naprawdę dobrze się czyta chyba zmienię zdanie.

0

W przykładzie testowym oczywiście jest błąd, ale chodzi mi o ideę

1 użytkowników online, w tym zalogowanych: 0, gości: 1