Injizieren des privaten Felds @Autowired während des Tests

75

Ich habe ein Komponenten-Setup, das im Wesentlichen ein Starter für eine Anwendung ist. Es ist wie folgt konfiguriert:

@Component
public class MyLauncher {
    @Autowired
    MyService myService;

    //other methods
}

MyService ist mit der @ServiceSpring-Annotation versehen und wird ohne Probleme automatisch in meine Launcher-Klasse übertragen.

Ich möchte einige jUnit-Testfälle für MyLauncher schreiben. Dazu habe ich eine Klasse wie diese gestartet:

public class MyLauncherTest
    private MyLauncher myLauncher = new MyLauncher();

    @Test
    public void someTest() {

    }
}

Kann ich ein Mock-Objekt für MyService erstellen und es in meiner Testklasse in myLauncher einfügen? Ich habe derzeit keinen Getter oder Setter in myLauncher, da Spring die automatische Verdrahtung übernimmt. Wenn möglich, möchte ich keine Getter und Setter hinzufügen müssen. Kann ich den Testfall anweisen, ein Scheinobjekt mithilfe einer @Beforeinit-Methode in die automatisch verdrahtete Variable einzufügen ?

Wenn ich das völlig falsch mache, kannst du das gerne sagen. Ich bin noch neu in diesem Bereich. Mein Hauptziel ist es, nur Java-Code oder Anmerkungen zu haben, die ein Scheinobjekt in diese @AutowiredVariable einfügen, ohne dass ich eine Setter-Methode schreiben oder eine applicationContext-test.xmlDatei verwenden muss. Ich würde viel lieber alles für die Testfälle in der .javaDatei pflegen, anstatt nur für meine Tests einen separaten Anwendungsinhalt pflegen zu müssen.

Ich hoffe, Mockito für die Scheinobjekte verwenden zu können. In der Vergangenheit habe ich dies getan, org.mockito.Mockitoindem ich meine Objekte mit verwendet und erstellt habe Mockito.mock(MyClass.class).

Kyle
quelle

Antworten:

81

Sie können MyLauncher in Ihrem Test absolut verspotten. Ich bin sicher, wenn Sie zeigen, welches spöttische Framework Sie verwenden, kann jemand schnell eine Antwort geben. Mit mockito würde ich die Verwendung von @RunWith (MockitoJUnitRunner.class) und die Verwendung von Anmerkungen für myLauncher untersuchen. Es würde ungefähr so ​​aussehen wie unten.

@RunWith(MockitoJUnitRunner.class)
public class MyLauncherTest
    @InjectMocks
    private MyLauncher myLauncher = new MyLauncher();

    @Mock
    private MyService myService;

    @Test
    public void someTest() {

    }
}
Manuel Quinones
quelle
@ jpganz18 mit junit verwenden Sie das normalerweise, es ist wie MockitoAnnotations.initMocks vor jeder Methode
aufzurufen
Im Moment verwende ich diesen Runner: @RunWith (SpringRunner.class) Kann ich den MockitoJUnitRunner ersetzen? Wenn nicht, gibt es eine Möglichkeit, die Mocks ohne sie zu injizieren? (Ich verwende nicht wirklich
Scheinobjekte
55

Die akzeptierte Antwort (benutze MockitoJUnitRunnerund @InjectMocks) ist großartig. Wenn Sie jedoch etwas Leichteres (kein spezieller JUnit-Läufer) und weniger "magisch" (transparenter) wünschen, insbesondere für den gelegentlichen Gebrauch, können Sie die privaten Felder einfach direkt mithilfe der Selbstbeobachtung festlegen.

Wenn Sie Spring verwenden, haben Sie bereits eine Utility-Klasse dafür: org.springframework.test.util.ReflectionTestUtils

Die Verwendung ist recht einfach:

ReflectionTestUtils.setField(myLauncher, "myService", myService);

Das erste Argument ist Ihre Ziel-Bean, das zweite ist der Name des (normalerweise privaten) Feldes und das letzte ist der zu injizierende Wert.

Wenn Sie Spring nicht verwenden, ist es ziemlich trivial, eine solche Dienstprogrammmethode zu implementieren. Hier ist der Code, den ich verwendet habe, bevor ich diese Spring-Klasse gefunden habe:

public static void setPrivateField(Object target, String fieldName, Object value){
        try{
            Field privateField = target.getClass().getDeclaredField(fieldName);
            privateField.setAccessible(true);
            privateField.set(target, value);
        }catch(Exception e){
            throw new RuntimeException(e);
        }
    }
Pierre Henry
quelle
1
Gute Antwort! Ich musste mvnrepository.com/artifact/org.springframework/spring-test in meine pom.xml aufnehmen, um die ReflectionTestUtils-Klasse zu erhalten.
user674669
1
Ich würde sagen, es ist ein schrecklicher Rat. Es gibt mindestens zwei Gründe, warum: 1. Alles, was Reflexion erfordert, riecht normalerweise schlecht und weist auf Architekturprobleme hin. 2. Der ganze Punkt der Abhängigkeitsinjektion besteht darin, injiziertes Objekt leicht ersetzen zu können. Verwenden Sie also Reflexion, bei der Sie diesen Punkt verpasst haben
artkoshelev
@artkoshelev Ich stimme zu, dass Konstruktionsinjekton zu Testzwecken und aus "architektonischen" Gründen sauberer ist und zu empfehlen ist. Es spielt auch besser mit Java-Konfiguration. Aber wenn Sie einige vorhandene Beans testen möchten, die Feldinjektion verwenden und diese nicht ändern können oder wollen, würde ich argumentieren, dass es besser ist, einen Test zu schreiben, der Reflexion im Setup verwendet, als überhaupt keinen Test ... Außerdem Wenn Reflexion immer ein Codegeruch oder ein Architekturproblem ist, dann ist Frühling ein Codegeruch, Ruhezustand ist ein Codegeruch und so weiter ...
Pierre Henry
"Außerdem, wenn Reflexion immer ein Codegeruch oder ein Architekturproblem ist, als Spring ein Codegeruch, ist Hibernate ein Codegeruch und so weiter ..." Ich habe sicherlich nicht über gut getestete und bekannte Frameworks gesprochen, aber Die tatsächlichen Entwickler des Anwendungscodes haben geschrieben.
Artkoshelev
Ich bevorzuge diese Antwort.
Andrei Rînea
17

Manchmal können Sie Ihre @ComponentKonstruktion mithilfe eines Konstruktors oder einer Setter-basierten Injektion umgestalten , um Ihren Testfall einzurichten (Sie können und können sich immer noch darauf verlassen @Autowired). Jetzt können Sie Ihren Test vollständig ohne ein spöttisches Framework erstellen, indem Sie stattdessen Teststubs implementieren (z. B. Martin Fowlers MailServiceStub ):

@Component
public class MyLauncher {

    private MyService myService;

    @Autowired
    MyLauncher(MyService myService) {
        this.myService = myService;
    }

    // other methods
}

public class MyServiceStub implements MyService {
    // ...
}

public class MyLauncherTest
    private MyLauncher myLauncher;
    private MyServiceStub myServiceStub;

    @Before
    public void setUp() {
        myServiceStub = new MyServiceStub();
        myLauncher = new MyLauncher(myServiceStub);
    }

    @Test
    public void someTest() {

    }
}

Diese Technik besonders nützlich , wenn der Test und die Klasse unter Test im gleichen Paket befinden sich denn dann können Sie die Standardeinstellung verwenden Paket-privaten Zugriffsmodifikator zu anderen Klassen zu verhindern , dass es erreichbar. Beachten Sie, dass Sie Ihren Produktionscode weiterhin haben können src/main/java, Ihre Tests jedoch in src/main/testVerzeichnissen.


Wenn Sie Mockito mögen, werden Sie den MockitoJUnitRunner zu schätzen wissen . Es ermöglicht Ihnen, "magische" Dinge zu tun, wie Ihnen @Manuel gezeigt hat:

@RunWith(MockitoJUnitRunner.class)
public class MyLauncherTest
    @InjectMocks
    private MyLauncher myLauncher; // no need to call the constructor

    @Mock
    private MyService myService;

    @Test
    public void someTest() {

    }
}

Alternativ können Sie den Standard-JUnit-Runner verwenden und MockitoAnnotations.initMocks () in einer setUp()Methode aufrufen , damit Mockito die mit Anmerkungen versehenen Werte initialisiert. Weitere Informationen finden Sie im Javadoc von @InitMocks und in einem Blog-Beitrag , den ich geschrieben habe.

matsev
quelle
Gute Antwort, Konstruktorinjektion ist der bevorzugte Weg und funktioniert gut mit Kotlin.
Malkaviano
2

Ich bin ein neuer Benutzer für Spring. Ich habe dafür eine andere Lösung gefunden. Reflektion verwenden und notwendige Felder öffentlich machen und Scheinobjekte zuweisen.

Dies ist mein Auth-Controller und er hat einige private Eigenschaften von Autowired.

@RestController
public class AuthController {

    @Autowired
    private UsersDAOInterface usersDao;

    @Autowired
    private TokensDAOInterface tokensDao;

    @RequestMapping(path = "/auth/getToken", method = RequestMethod.POST)
    public @ResponseBody Object getToken(@RequestParam String username,
            @RequestParam String password) {
        User user = usersDao.getLoginUser(username, password);

        if (user == null)
            return new ErrorResult("Kullanıcıadı veya şifre hatalı");

        Token token = new Token();
        token.setTokenId("aergaerg");
        token.setUserId(1);
        token.setInsertDatetime(new Date());
        return token;
    }
}

Und dies ist mein Junit-Test für AuthController. Ich mache öffentlich benötigte private Immobilien und weise ihnen Scheinobjekte zu und rocke :)

public class AuthControllerTest {

    @Test
    public void getToken() {
        try {
            UsersDAO mockUsersDao = mock(UsersDAO.class);
            TokensDAO mockTokensDao = mock(TokensDAO.class);

            User dummyUser = new User();
            dummyUser.setId(10);
            dummyUser.setUsername("nixarsoft");
            dummyUser.setTopId(0);

            when(mockUsersDao.getLoginUser(Matchers.anyString(), Matchers.anyString())) //
                    .thenReturn(dummyUser);

            AuthController ctrl = new AuthController();

            Field usersDaoField = ctrl.getClass().getDeclaredField("usersDao");
            usersDaoField.setAccessible(true);
            usersDaoField.set(ctrl, mockUsersDao);

            Field tokensDaoField = ctrl.getClass().getDeclaredField("tokensDao");
            tokensDaoField.setAccessible(true);
            tokensDaoField.set(ctrl, mockTokensDao);

            Token t = (Token) ctrl.getToken("test", "aergaeg");

            Assert.assertNotNull(t);

        } catch (Exception ex) {
            System.out.println(ex);
        }
    }

}

Ich kenne keine Vor- und Nachteile für diesen Weg, aber das funktioniert. Diese Technik hat etwas mehr Code, aber diese Codes können durch verschiedene Methoden usw. getrennt werden. Es gibt mehr gute Antworten auf diese Frage, aber ich möchte auf eine andere Lösung hinweisen. Entschuldigung für mein schlechtes Englisch. Habt alle ein gutes Java :)

Kodmanyagha
quelle
Ich bin auch relativ neu im Frühling. Die Verwendung @Autowiredscheint das Testen wirklich einfach zu machen. Ich bin mir aber auch nicht sicher, ob es die "richtige" Lösung ist.
AnonymousAngelo
Die Frage ist über Injektion
MK-Rou
2

Ich glaube, damit Ihre MyLauncher-Klasse (für myService) automatisch verkabelt werden kann, müssen Sie Spring sie initialisieren lassen, anstatt den Konstruktor aufzurufen, indem Sie myLauncher automatisch verkabeln. Sobald dies automatisch verkabelt wird (und myService auch automatisch verkabelt wird), bietet Spring (1.4.0 und höher) eine @ MockBean-Annotation, die Sie in Ihren Test einfügen können. Dadurch werden übereinstimmende einzelne Beans im Kontext durch ein Modell dieses Typs ersetzt. Anschließend können Sie in einer @ Before-Methode weiter definieren, welche Verspottung Sie wünschen.

public class MyLauncherTest
    @MockBean
    private MyService myService;

    @Autowired
    private MyLauncher myLauncher;

    @Before
    private void setupMockBean() {
        doNothing().when(myService).someVoidMethod();
        doReturn("Some Value").when(myService).someStringMethod();
    }

    @Test
    public void someTest() {
        myLauncher.doSomething();
    }
}

Ihre MyLauncher-Klasse kann dann unverändert bleiben, und Ihre MyService-Bean ist ein Mock, dessen Methoden Werte zurückgeben, wie Sie sie definiert haben:

@Component
public class MyLauncher {
    @Autowired
    MyService myService;

    public void doSomething() {
        myService.someVoidMethod();
        myService.someMethodThatCallsSomeStringMethod();
    }

    //other methods
}

Ein paar Vorteile gegenüber anderen genannten Methoden sind:

  1. Sie müssen myService nicht manuell injizieren.
  2. Sie müssen weder den Mockito-Läufer noch die Regeln verwenden.
snydergd
quelle
Nicht @MockBeanbrauchen @RunWith(SpringRunner.class)?
Wonsuc
1

Schauen Sie sich diesen Link an

Dann schreiben Sie Ihren Testfall als

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"/applicationContext.xml"})
public class MyLauncherTest{

@Resource
private MyLauncher myLauncher ;

   @Test
   public void someTest() {
       //test code
   }
}
NullPointerException
quelle
Dies ist der richtige Weg, um den Spring Context zu laden !! aber ich denke besser, Sie erstellen einen
Testkontext
Dies ist nicht unbedingt der richtige Weg, alles hängt davon ab, was Sie erreichen möchten. Was ist, wenn Sie Ruhezustandsitzungen in Ihrem App-Kontext einrichten? Möchten Sie wirklich eine echte Datenbank mit einem Unit-Test erreichen? Einige Leute sagen möglicherweise "Ja", ohne zu bemerken, dass sie einen Integrationstest erstellen und möglicherweise ihre Daten durcheinander bringen. Wenn Sie dagegen einen Test-App-Kontext erstellt und den Ruhezustand auf eine eingebettete Datenbank verweisen, sind Ihre Daten besser dran, aber Sie erstellen immer noch einen Integrationstest, keinen Komponententest.
Dan
Dies ist für sogenannte "Integrationstests", wenn Sie Ihre Komponenten zusammen mit dem gesamten Kontext testen möchten. Dies ist natürlich nützlich, aber nicht dasselbe wie ein Komponententest, bei dem Sie eine einzelne Klasse isoliert testen möchten.
Pierre Henry