Mockito mengejek kelas final lokal tetapi gagal di Jenkins

11

Saya telah menulis beberapa tes unit untuk metode statis. Metode statis hanya membutuhkan satu argumen. Jenis argumen adalah kelas akhir. Dalam hal kode:

public class Utility {

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

public final class Customer {
   // class definition
}

Jadi untuk Utilitykelas saya telah membuat kelas tes UtilityTestsdi mana saya telah menulis tes untuk metode ini getName,. Kerangka pengujian unit adalah TestNG dan perpustakaan mocking yang digunakan adalah Mockito. Jadi tes khas memiliki struktur berikut:

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(...);
   }
}

Apa masalahnya ?

Sedangkan tes berjalan dengan sukses secara lokal, di dalam IntelliJ, mereka gagal pada Jenkins (ketika saya mendorong kode saya di cabang jarak jauh, sebuah build dipicu dan unit test dijalankan pada akhirnya). Pesan kesalahannya sth seperti berikut:

org.mockito.exceptions.base.MockitoException: Tidak dapat mengejek / memata-matai kelas com.packagename.Pelanggan Mockito tidak dapat mengejek / memata-matai karena: - kelas akhir

Apa yang saya coba?

Saya mencari sedikit, untuk menemukan solusi tetapi saya tidak berhasil. Saya perhatikan di sini bahwa saya tidak diizinkan mengubah fakta bahwa Customerini adalah kelas akhir . Selain itu, saya ingin jika mungkin untuk tidak mengubah desainnya sama sekali (misalnya membuat antarmuka, yang akan menampung metode yang ingin saya tiru dan menyatakan bahwa kelas Pelanggan mengimplementasikan antarmuka itu, seperti yang ditunjukkan oleh Jose dengan tepat dalam bukunya). komentar). Hal yang saya coba adalah pilihan kedua yang disebutkan di mockito-final . Terlepas dari kenyataan bahwa ini memperbaiki masalah, rem beberapa tes unit lain :(, yang tidak dapat diperbaiki dengan cara yang tidak jelas.

Pertanyaan

Jadi, inilah dua pertanyaan yang saya miliki:

  1. Bagaimana itu mungkin terjadi? Bukankah seharusnya tes gagal baik lokal maupun Jenkins?
  2. Bagaimana ini dapat diperbaiki berdasarkan kendala yang saya sebutkan di atas?

Terima kasih sebelumnya atas bantuannya.

Christos
sumber
1
Dugaan saya adalah bahwa enable finalkonfigurasi berfungsi di ruang kerja Anda, tetapi ketika dijalankan Jenkinstidak dapat menemukan file ini. Periksa di mana Jenkinsmencari file dan apakah itu benar-benar ada atau tidak.
kedua
Utas lainnya ini menjelaskan cara mengaktifkan mengejek kelas akhir di Mockito 2, dengan menambahkan file konfigurasi mockito di bawah direktori sumber daya: stackoverflow.com/questions/14292863/…
Jose Tepedino
3
Apakah mungkin, dalam kode yang Anda hadapi, untuk mengekstrak antarmuka dari kelas Pelanggan, katakanlah ICustomer, dan menggunakannya di kelas Utilitas? Maka Anda bisa mengejek antarmuka itu alih-alih kelas akhir yang konkret
Jose Tepedino
@JoseTepedino Ini adalah poin yang valid. Itu benar-benar masuk akal dan ini jelas cara yang elegan untuk mengatasi masalah ini. Namun saya bertanya-tanya apakah ada cara lain dan yang lebih penting, saya ingin memahami mengapa pendekatan saat ini berhasil secara lokal dan gagal di Jenkins.
Christos
1
Apakah Customerada logika di dalamnya, atau hanya kelas data yang bodoh? Jika hanya sekelompok bidang dengan getter dan setter, maka Anda bisa langsung instantiate.
Willis Blackburn

Jawaban:

2

Pendekatan alternatif adalah dengan menggunakan pola 'metode untuk kelas'.

  1. Pindahkan metode dari kelas pelanggan ke kelas / kelas lain, misalkan CustomerSomething misalnya / CustomerFinances (atau apa pun tanggung jawabnya).
  2. Tambahkan konstruktor ke Pelanggan.
  3. Sekarang Anda tidak perlu mengejek Pelanggan, hanya kelas CustomerSomething! Anda mungkin tidak perlu mengejeknya jika tidak memiliki dependensi eksternal.

Inilah blog yang bagus tentang topik ini: https://simpleprogrammer.com/back-to-basics-mock-eliminating-patterns/

Johnny Alpha
sumber
1
Terima kasih atas jawaban Anda (+1). Saya menemukan cara untuk memperbaikinya (jawaban untuk pertanyaan kedua). Namun alasan mengapa tes gagal di dalam IntelliJ masih belum jelas bagi saya. Lebih jauh lagi, saya tidak dapat mereproduksi lagi (kegagalan di dalam IntelliJ), yang benar-benar aneh.
Christos
1

Bagaimana itu mungkin terjadi? Bukankah seharusnya tes gagal baik lokal maupun Jenkins?

Ini jelas semacam env-spesifik. Satu-satunya pertanyaan adalah - bagaimana menentukan penyebab perbedaan.

Saya sarankan Anda untuk memeriksa org.mockito.internal.util.MockUtil#typeMockabilityOfmetode dan membandingkan, apa mockMakeryang sebenarnya digunakan di kedua lingkungan dan mengapa.

Jika mockMakersama - bandingkan kelas yang dimuat IDE-Clientvs Jenkins-Client- apakah mereka memiliki perbedaan pada waktu pelaksanaan tes.

Bagaimana ini dapat diperbaiki berdasarkan kendala yang saya sebutkan di atas?

Kode berikut ditulis dengan asumsi OpenJDK 12 dan Mockito 2.28.2, tetapi saya yakin Anda dapat menyesuaikannya dengan versi yang benar-benar digunakan.

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);
        }
    }    
}

Dengan aturan terpisah untuk inline mock:

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
sumber
Terima kasih atas jawaban Anda (+1). Saya menemukan cara untuk memperbaikinya (jawaban untuk pertanyaan kedua). Namun alasan mengapa tes gagal di dalam IntelliJ masih belum jelas bagi saya. Lebih jauh lagi, saya tidak dapat mereproduksi lagi (kegagalan di dalam IntelliJ), yang benar-benar aneh.
Christos
1

Pastikan Anda menjalankan tes dengan argumen yang sama. Periksa apakah konfigurasi menjalankan intellij Anda cocok dengan jenkins. https://www.jetbrains.com/help/idea/creating-and-editing-run-debug-configurations.html . Anda dapat mencoba menjalankan tes pada mesin lokal dengan argumen yang sama seperti pada jenkins (dari terminal), jika gagal maka itu berarti masalahnya ada pada argumen

Tautan182
sumber
File org.mockito.plugins.MockMakeritu ada juga di mesin jenkins. Saya menggunakan JVM yang sama di mesin bot. Saya akan memeriksa 3 yang Anda tunjukkan. Terima kasih
Christos
Saya mencoba menjalankan tes melalui konsol, menggunakan perintah yang digunakan dalam Jenkins. Mereka gagal dengan pesan kesalahan yang sama persis. Jadi sesuatu yang aneh terjadi di dalam IntelliJ.
Christos
Lihatlah .idea / workspace.xml di proses run Anda, ada di dalam tag <component>. Setelah itu Anda bisa belajar bagaimana mengubah xml itu menjadi perintah bash
Link182
Bisakah Anda menunjukkan perintah terminal jenkins yang digunakan untuk menjalankan tes? Bisakah Anda memberi tahu saya manajer paket mana yang Anda gunakan?
Link182
Sebagai alat pembangun, saya menggunakan Gradle.
Christos