Instantiating objects when using Spring, for testing vs production

孤街醉人 提交于 2019-12-12 09:39:15

问题


Am correct in understanding that when using Spring, you should use the Spring configuration xml to instantiate your objects for production, and directly instantiate objects when testing?

Eg.

MyMain.java

package org.world.hello;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MyMain {

    private Room room;


    public static void speak(String str)
    {
        System.out.println(str);
    }

    public static void main(String[] args) {

        ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
        Room room = (Room) context.getBean("myRoom");

        speak(room.generatePoem());


    }

}

Room.java

package org.world.hello;

public class Room {

    private BottleCounter bottleCounter;
    private int numBottles;

    public String generatePoem()
    {
        String str = "";
        for (int i = numBottles; i>=0; i--)
        {
            str = str +  bottleCounter.countBottle(i) + "\n";

        }
        return str;
    }

    public BottleCounter getBottleCounter() {
        return bottleCounter;
    }

    public void setBottleCounter(BottleCounter bottleCounter) {
        this.bottleCounter = bottleCounter;
    }

    public int getNumBottles() {
        return numBottles;
    }

    public void setNumBottles(int numBottles) {
        this.numBottles = numBottles;
    }

}

BottleCounter.java

package org.world.hello;

public class BottleCounter {

    public String countBottle(int i)
    {
        return i + " bottles of beer on the wall" + i + " bottles of beer!";
    }

}

Beans.xml:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

   <bean id="myRoom" class="org.world.hello.Room">
       <property name="bottleCounter">
            <bean id = "myBottleCounter" class = "org.world.hello.BottleCounter"/>     
       </property>
       <property name = "numBottles" value = "10"></property>

   </bean>

</beans>

Outputs: (my apologies for the missing space)

10 bottles of beer on the wall10 bottles of beer!
9 bottles of beer on the wall9 bottles of beer!
8 bottles of beer on the wall8 bottles of beer!
7 bottles of beer on the wall7 bottles of beer!
6 bottles of beer on the wall6 bottles of beer!
5 bottles of beer on the wall5 bottles of beer!
4 bottles of beer on the wall4 bottles of beer!
3 bottles of beer on the wall3 bottles of beer!
2 bottles of beer on the wall2 bottles of beer!
1 bottles of beer on the wall1 bottles of beer!
0 bottles of beer on the wall0 bottles of beer!

Now for testing this:

BottleCounterTest.java:

package org.world.hello;

import static org.junit.Assert.*;

import org.junit.Test;

public class BottleCounterTest {

    @Test
    public void testOneBottle() {
        BottleCounter b = new BottleCounter();
        assertEquals("1 bottles of beer on the wall1 bottles of beer!", b.countBottle(1));
    }

}

Pretty straight forward.

RoomTest.java:

package org.world.hello;

import static org.junit.Assert.*;
import org.mockito.Mockito;
import org.junit.Test;

public class RoomTest {

    @Test
    public void testThreeBottlesAreSeperatedByNewLines()
    {
        Room r = new Room();
        BottleCounter b = Mockito.mock(BottleCounter.class);
        Mockito.when(b.countBottle(Mockito.anyInt())).thenReturn("a");
        r.setBottleCounter(b);
        r.setNumBottles(3);
        assertEquals("a\na\na\na\n", r.generatePoem());
    }

}

Am I correct in instantiating my test objects this way?


回答1:


Inner static class configuration: When testing Spring components we usually use @RunWith(SpringJUnit4ClassRunner.class) and make our class @ContextConfiguration. By making class @ContextConfiguration you can create an inner static class for configuration and in it you have full control. There you define all you need as beans and @Autowired it in your test, along with dependencies which can be mocks or regular objects, depending on test case.

Component scanning production code: If there are more components needed for test you can add @ComponentScan but we try to make it scan only packages it needs (this is when you use @Component annotation but in your case you can add XML to @ContextConfiguration). This is a good choice when you do not need mocks and you have a complicated setup which needs to be production like. This is good for integration tests where you want to test how components interact with each other in functional slices you want to test.

Hybrid approach: This is the usual case when you have many beans which need to be production like but one or two need to be mocks. Then you can @ComponentScan production code but add an inner static class which is @Configuration and there define beans with annotation @Primary which will override production code configuration for that bean in case of tests. This is good since you do not need to write long @Configuration with all defined beans, you scan what you need and override what should be mocked.

In your case I would go with first approach like this:

package org.world.hello;

import static org.junit.Assert.*;
import org.mockito.Mockito;
import org.junit.Test;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class RoomTest {

    @Configuration
    //@ImportResource(value = {"path/to/resource.xml"}) if you need to load additional xml configuration
    static class TestConfig {
       @Bean
       public BottleCounter bottleCounter() {
        return Mockito.mock(BottleCounter.class);
       }

       @Bean
       public Room room(BottleCounter bottleCounter) {
         Room room = new Room();
         room.setBottleCounter(bottleCounter);
         //r.setNumBottles(3); if you need 3 in each test
         return room;           
       }
    }

    @Autowired
    private Room room;  //room defined in configuration with mocked bottlecounter

    @Test
    public void testThreeBottlesAreSeperatedByNewLines()
    {
        Mockito.when(b.countBottle(Mockito.anyInt())).thenReturn("a");
        r.setNumBottles(3);
        assertEquals("a\na\na\na\n", r.generatePoem());
    }

}



回答2:


In general when you want to create the unit testing, you need to have in mind :

  1. You need to test the code for the real object, it means the class that you want to unit test need to be a real instance, it is not ideal using new operator as you probably have some dependencies in the object and using constructor is not always the better way. But you could use something like this.

    @Before
    public void init(){
       room = new Room(Mockito.mock(BottleCounter.class)); //If you have a constructor that receive the dependencies
    }
    
  2. All the member variables that are other object (aka. as dependencies) need to be mocked, any has-a relationship need to be replace with a Mock object and all the calls to the methods of this mocked object should be mocked as well using Mockito.when

If you use

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring-config.xml")

You will call your real beans and that wont be a unit testing, it will be more like integration testing. In the example that you write in your question from my point of view the test should be done as:

@RunWith(MockitoJUnitRunner.class)
public class RoomTest {

@InjectMocks 
public Room room; //This will instantiate the real object for you
                  //So you wont need new operator anymore.

@Mock   //You wont need this in your class example
private AnyDependecyClass anyDependency;

@Test
public void testThreeBottlesAreSeperatedByNewLines(){
    BottleCounter b = Mockito.mock(BottleCounter.class);
    Mockito.when(b.countBottle(Mockito.anyInt())).thenReturn("a");
    room.setBottleCounter(b);
    room.setNumBottles(3);
    assertEquals("a\na\na\na\n", room.generatePoem());
   }
}



回答3:


I guess, this is not the right way of testing Junit in Spring as you are creating Room object using new keyword in your RoomTest.java .

You can use your same configuration file i.e Beans.xml file to create bean during Junit test cases.

Spring provides @RunWith and @ContextConfiguration to perform above task. Check here for more detail explaination.




回答4:


In my oppinion Dependency Injectio should make your code less dependent on the container than it would be with traditional Java EE development.

The POJOs that make up your application should be testable in JUnit or TestNG tests, with objects simply instantiated using the new operator, without Spring or any other container.

For instance:

import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class RoomTest {

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

    @Mock   //You wont need this in your class example
    private BottleCounter nameOfBottleCounterAttributeInsideRoom;

    @InjectMocks 
    public Room room;

   @Test
   public void testThreeBottlesAreSeperatedByNewLines(){
      when(b.countBottle(anyInt())).thenReturn("a");
      room.setBottleCounter(b);
      room.setNumBottles(3);
      assertEquals("a\na\na\na\n", room.generatePoem());
   }
}



回答5:


First an answer

You should run your test with a Spring test runner, using a test specific context

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:test-context.xml")

Let Spring instantiate your bean, but tailor your test specific context so that it excludes all the beans that you don't need inside a test, or mock-away the stuff that you don't want to test (e.g. your BottleCounter) but can't exclude

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <!--Mock BottleCounter -->
    <bean id="myBottleCounter" name="myBottleCounter" class="org.mockito.Mockito" factory-method="mock">
        <constructor-arg value="org.world.hello.BottleCounter"/>
    </bean>

   <bean id="myRoom" class="org.world.hello.Room">
       <property name="bottleCounter" ref="myBottleCounter"></property>
       <property name = "numBottles" value = "10"></property>
   </bean>
</beans>

and another note, in production, you'll most likely end up with annotated beans that are being picked up by spring based on scanning the classpath for annotated classes as oppose to declaring them all in xml. In this setup, you can still mock your beans with a help of context:exclude-filter, something like

<!--Mock BottleCounter -->
<bean id="myBottleCounter" name="myBottleCounter" class="org.mockito.Mockito" factory-method="mock">
   <constructor-arg value="org.world.hello.BottleCounter"/>
</bean>
<context:component-scan base-package="org.world.hello">
   <context:exclude-filter type="regex" expression="org\.world\.hello\.Bottle*"/>
</context:component-scan>

More about your dilemma

In my view you have setup the context for a dilemma wrong. When you say am I correct in understanding that when using Spring, you should use the Spring configuration xml to instantiate your objects for production, and directly instantiate objects when testing. There can be only one answer, yes, you're wrong, because this is not related to Spring at all.

The context where your dilemma is valid is when you reason about integration vs unit testing. In particular, if you define that unit test is testing an individual component with everything else (including dependencies to other beans) being mocked or stubbed away. So if your intent is to write the unit test according to this definiton, your code is perfectly OK, even desirable cause by instantiating the object directly, no framework will be able to automagically inject its dependencies. According to this definition spring tests are integration tests, and that is what @Koitoer mentions in his answer when he says You will call your real beans and that wont be a unit testing, it will be more like integration testing

In practice, people are usually not concerned about the distinction. Spring refers to its test as unit tests. The common case is what @Nenad Bozic calls a hybrid approch, where you would like ot mock out just a few object, e.g. connection to a DB or the like, and based on some of your comments this is what you need.



来源:https://stackoverflow.com/questions/29203218/instantiating-objects-when-using-spring-for-testing-vs-production

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!