Mockito wyśmiewa lokalnie ostatnią klasę, ale zawodzi w Jenkins

11

Napisałem kilka testów jednostkowych dla metody statycznej. Metoda statyczna przyjmuje tylko jeden argument. Typ argumentu jest klasą końcową. Pod względem kodu:

public class Utility {

   public static Optional<String> getName(Customer customer) {
       // method's body.
   }
}

public final class Customer {
   // class definition
}

Tak dla Utilityklasy I stworzyliśmy klasę testową UtilityTests, w której mam napisane testy na tej metodzie getName. Środowisko testowania jednostkowego to TestNG, a używana jest fałszywa biblioteka Mockito. Typowy test ma więc następującą strukturę:

public class UtilityTests {

   @Test
   public void getNameTest() {
     // Arrange
     Customer customerMock = Mockito.mock(Customer.class);
     Mockito.when(...).thenReturn(...);

     // Act
     Optional<String> name = Utility.getName(customerMock);

     // Assert
     Assert.assertTrue(...);
   }
}

Jaki jest problem ?

Podczas gdy testy przebiegają pomyślnie lokalnie, wewnątrz IntelliJ, nie działają na Jenkinsie (kiedy pcham mój kod w zdalnej gałęzi, uruchamiana jest kompilacja, a testy jednostkowe uruchamiane na końcu). Komunikat o błędzie jest następujący:

org.mockito.exceptions.base.MockitoException: Nie można wyśmiewać / szpiegować klasy com.packagename. Klient Mockito nie może wyśmiewać / szpiegować, ponieważ: - klasa ostateczna

Co próbowałem

Szukałem trochę, aby znaleźć rozwiązanie, ale nie udało mi się. Zaznaczam tutaj, że nie wolno mi zmieniać faktu, że Customerjest to klasa ostateczna . Poza tym chciałbym, jeśli to możliwe, w ogóle nie zmieniać jego projektu (np. Stworzyć interfejs, który przechowuje metody, które chcę wyśmiewać i stwierdza, że ​​klasa Customer implementuje ten interfejs, jak słusznie zauważył Jose w swoim komentarz). To, czego próbowałem, to druga opcja wspomniana na finale mockito . Pomimo tego, że rozwiązało to problem, hamuje kilka innych testów jednostkowych :(, których nie można naprawić w żaden widoczny sposób.

pytania

Oto dwa pytania, które mam:

  1. Jak to w ogóle możliwe? Czy test nie powinien zawieść zarówno lokalnie, jak i w Jenkins?
  2. Jak można to naprawić w oparciu o ograniczenia, o których wspomniałem powyżej?

Z góry dziękuję za wszelką pomoc.

Christos
źródło
1
Domyślam się, że enable finalkonfiguracja działa w twoim obszarze roboczym, ale po uruchomieniu Jenkinsnie można znaleźć tego pliku. Sprawdź, gdzie Jenkinsszuka pliku i czy rzeczywiście tam jest, czy nie.
drugi
Ten drugi wątek wyjaśnia, jak włączyć końcowe kpiny z klasy w Mockito 2, dodając plik konfiguracyjny mockito w katalogu zasobów: stackoverflow.com/questions/14292863/...
Jose Tepedino,
3
Czy w kodzie, z którym mamy do czynienia, byłoby możliwe wyodrębnienie interfejsu z klasy Customer, powiedzmy ICustomer, i użycie go w klasie Utility? Następnie możesz wyśmiewać ten interfejs zamiast konkretnej klasy końcowej
Jose Tepedino,
@JoseTepedino To jest poprawny punkt. Ma to całkowicie sens i jest zdecydowanie eleganckim sposobem na rozwiązanie tego problemu. Zastanawiam się jednak, czy istnieje inny sposób i, co ważniejsze, chcę zrozumieć, dlaczego obecne podejście odnosi sukces lokalnie, a nie w Jenkins.
Christos,
1
Czy Customerma w tym jakąś logikę, czy to tylko głupia klasa danych? Jeśli to tylko kilka pól z modułami pobierającymi i ustawiającymi, możesz po prostu utworzyć jego instancję.
Willis Blackburn,

Odpowiedzi:

2

Alternatywnym podejściem byłoby użycie wzorca „metoda do klasyfikacji”.

  1. Przenieś metody z klasy klienta do innej klasy / klas, powiedz CustomerSomething np. / CustomerFinances (lub cokolwiek to jest odpowiedzialność).
  2. Dodaj konstruktora do klienta.
  3. Teraz nie musisz kpić z klienta, tylko klasa CustomerSomething! Być może nie będziesz musiał kpić z tego, jeśli nie ma zewnętrznych zależności.

Oto dobry blog na ten temat: https://simpleprogrammer.com/back-to-basics-mock- Eliminating-patterns/

Johnny Alpha
źródło
1
Dziękuję za odpowiedź (+1). Znalazłem sposób, aby to naprawić (odpowiedź na drugie pytanie). Jednak powód niepowodzenia testów w IntelliJ nadal nie jest dla mnie jasny. Co więcej, nie mogę go już odtworzyć (awaria w IntelliJ), co jest całkowicie dziwne.
Christos,
1

Jak to w ogóle możliwe? Czy test nie powinien zawieść zarówno lokalnie, jak i w Jenkins?

To oczywiście rodzaj env-specyfiki. Jedyne pytanie brzmi - jak ustalić przyczynę różnicy.

Sugeruję sprawdzenie org.mockito.internal.util.MockUtil#typeMockabilityOfmetody i porównanie, co mockMakerjest faktycznie używane w obu środowiskach i dlaczego.

Jeśli mockMakerjest to samo - porównaj załadowane klasy IDE-Clientvs Jenkins-Client- czy mają one jakąkolwiek różnicę w czasie wykonywania testu.

Jak można to naprawić w oparciu o ograniczenia, o których wspomniałem powyżej?

Poniższy kod został napisany przy założeniu OpenJDK 12 i Mockito 2.28.2, ale wierzę, że można go dostosować do dowolnej faktycznie używanej wersji.

public class UtilityTest {    
    @Rule
    public InlineMocksRule inlineMocksRule = new InlineMocksRule();

    @Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();

    @Test
    public void testFinalClass() {
        // Given
        String testName = "Ainz Ooal Gown";
        Client client = Mockito.mock(Client.class);
        Mockito.when(client.getName()).thenReturn(testName);

        // When
        String name = Utility.getName(client).orElseThrow();

        // Then
        assertEquals(testName, name);
    }

    static final class Client {
        final String getName() {
            return "text";
        }
    }

    static final class Utility {
        static Optional<String> getName(Client client) {
            return Optional.ofNullable(client).map(Client::getName);
        }
    }    
}

Z osobną regułą dla próbnych wstawek:

import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.mockito.internal.configuration.plugins.Plugins;
import org.mockito.internal.util.MockUtil;

import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

public class InlineMocksRule implements TestRule {
    private static Field MOCK_MAKER_FIELD;

    static {
        try {
            MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Field.class, MethodHandles.lookup());
            VarHandle modifiers = lookup.findVarHandle(Field.class, "modifiers", int.class);

            MOCK_MAKER_FIELD = MockUtil.class.getDeclaredField("mockMaker");
            MOCK_MAKER_FIELD.setAccessible(true);

            int mods = MOCK_MAKER_FIELD.getModifiers();
            if (Modifier.isFinal(mods)) {
                modifiers.set(MOCK_MAKER_FIELD, mods & ~Modifier.FINAL);
            }
        } catch (IllegalAccessException | NoSuchFieldException ex) {
            throw new RuntimeException(ex);
        }
    }

    @Override
    public Statement apply(Statement base, Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                Object oldMaker = MOCK_MAKER_FIELD.get(null);
                MOCK_MAKER_FIELD.set(null, Plugins.getPlugins().getInlineMockMaker());
                try {
                    base.evaluate();
                } finally {
                    MOCK_MAKER_FIELD.set(null, oldMaker);
                }
            }
        };
    }
}
ursa
źródło
Dziękuję za odpowiedź (+1). Znalazłem sposób, aby to naprawić (odpowiedź na drugie pytanie). Jednak powód niepowodzenia testów w IntelliJ nadal nie jest dla mnie jasny. Co więcej, nie mogę go już odtworzyć (awaria w IntelliJ), co jest całkowicie dziwne.
Christos,
1

Upewnij się, że uruchomiłeś test z tymi samymi argumentami. Sprawdź, czy konfiguracje uruchamiania Intellij pasują do jenkins. https://www.jetbrains.com/help/idea/creating-and-editing-run-debug-configurations.html . Możesz spróbować uruchomić test na komputerze lokalnym z tymi samymi argumentami, co na jenkins (z terminala), jeśli się nie powiedzie, oznacza to, że problem jest w argumentach

Link182
źródło
Plik org.mockito.plugins.MockMakeristnieje również w maszynie Jenkinsa. Używam tej samej maszyny JVM w botach. Sprawdzę 3, które wskazałeś. Dzięki
Christos,
Próbowałem uruchomić test za pomocą konsoli, używając polecenia użytego w Jenkins. Nie udaje im się ten sam dokładny komunikat o błędzie. Coś dziwnego dzieje się w IntelliJ.
Christos,
Spójrz na .idea / workspace.xml przy konfiguracji uruchamiania, jest on wewnątrz znacznika <component>. Następnie możesz dowiedzieć się, jak przekształcić ten xml w polecenie bash
Link182
Czy możesz pokazać komendę Jenkins terminal, która służy do uruchamiania testów? Czy możesz mi również powiedzieć, którego menedżera pakietów używasz?
Link182,
Jako narzędzie do budowania używam Gradle.
Christos,