从单元测试中设置依赖关系的最佳策略?

时间:2011-07-18 13:24:33

标签: java unit-testing

假设一个类Foo,其依赖项Bar由一些DI-Framework注入(在本例中为CDI):

public class Foo {

   @Inject
   private Bar bar;
   ...
   public void someLogic() {
      ...
   }

}

让 - 为了示例 - Bar是一个可模拟的基础结构依赖,如EntityManagerUserTransaction超出范围单元测试,但是 - 再次为了示例 - 要求Foo正确初始化。

假设一个单元测试 FooTest(我明确地希望编写没有容器依赖的单元测试),这需要以某种方式设置或模拟依赖项,但是否则试图在所需的容器功能方面保持低调(因此不是集成测试)。

public class FooTest {

   private Foo foo;

   @BeforeClass
   public void initFoo() {
      ... set bar somehow ...
   }

   @Test
   public doSomeTestOnSomeMethod() {
      ...
   }

}

我正在尝试收集一些最佳策略的事实,以便在我的测试中设置这些容器控制的依赖项:

(1)将getBar()setBar()添加到Foo

( - )不必要的膨胀代码(?) (+)直截了当 (+)适用​​于所有(测试)类

(2)使用反射来设置bar

( - )复杂,难以阅读&保持 (+)适用​​于所有(测试)类

(3)删除private限定符以使这些字段可以在测试中访问(假设相同的包)

(+)最简单,最短的解决方案 ( - )不是生产代码中最小的范围 ( - )不能在包装外使用

关于这个话题有没有明确的建议?

更新:我知道我可以使用容器作为测试的依赖提供程序 - 我这样做是为了集成测试,但我当然不希望完整的Java EE堆栈单元测试。 我希望这不会导致框架的火焰; - )

Update2:感谢大家回答到目前为止,但我似乎已经表达了自己的不清楚。答案往往表明我没有充分利用底层DI框架的可能性,或者只是需要更多的嘲弄。这当然都是好的和真实的,但我更愿意就是否可以将类成员的可见性扩展到默认级别以便利用可见性,如果“测试专有”-getter-setter方法更好,或者如果反思(我目前的解决方案)是要走的路。试图编辑问题,以便更好地描述我的问题。

恢复和关闭如果我拥有适当的权利,我非常想关闭这个问题,因为我显然是以一种非常误导和某种混乱的方式表达自己。我既不想在测试中讨论正确的依赖注入,也不想要任何代码片段。我 - 光荣地失败了 - 意图是开始讨论我已经在更新2中写过的内容。感谢所有贡献的人 - 我将在未来的问题中付出更多的努力。

4 个答案:

答案 0 :(得分:3)

我认为您应该更改代码,以便通过构造函数声明此依赖项,并让DI框架将其注入其中,然后在测试中您只需传递模拟。

以它当前的方式使得对此事物的依赖性“隐藏”,消费代码无法知道需要Bar,这会使代码混乱且难以理解。

这也意味着您依赖于DI框架,而不是手动构建对象图形。如果你想

,看起来这可能会让改变DI框架变得困难

答案 1 :(得分:2)

我会使用你的DI注入框架来创建Foo并初始化吧。这就是您打算如何使用它。


您也可以在不改变反射代码的情况下进行设置。

public static void set(Object o, String field, Object value) {
    for (Class c = o.getClass(); c != null; c = c.getSuperclass())
        try {
            Field f = c.getDeclaredField(field);
            f.setAccessible(true);
            f.set(o, value);
            return;
        } catch (NoSuchFieldException ignored) {
            if (c == Object.class)
                throw new AssertionError(e);
            continue;
        } catch (Exception e) {
            throw new AssertionError(e);
        }
}

Foo foo = new Foo();
set(foo, "bar", new Bar());

不是将字段公开或广泛访问,而是可以保护字段并通过子类设置它。

 public class Foo {
     protected Bar bar;
 }

 // in the test
 Foo foo = new Foo() {{
     bar = new Bar();
 }};
 // test the foo

这样原始类不会过多,但你可以通过子类测试它的功能。

答案 2 :(得分:2)

在正在测试的类中添加一个setter,在单元测试中,注入Bar类的a mock version。 Mockito,EasyMock,JMock都让这很简单。

如果Foo类没有setter,那么任何人都可以在容器外使用它?将安装者标记为“膨胀”对我来说听起来过于夸张。我相信你是在推翻这种情况。

您的更新更新:

  

这当然都是好的和真实的,但我更愿意收集意见,是否可以将类成员的可见性扩展到默认级别,以便利用可见性,如果getter-setter方法更好,或者反射(我现在的解决方案)是要走的路。

我个人认为添加一个setter只是一些额外的按键(大多数IDE会为你自动化),而改变字段的可见性可能导致其他糟糕的选择(突然有人决定他们可以写直接使用该字段的生产代码,因为你没有使它成为private),并且使用反射需要更多的代码(并且比使用setter更慢)。 setBar(Bar)方法就像它获得的一样简单。

答案 3 :(得分:1)

我知道您不想在测试中导入所有Java EE堆栈,但如果您想测试注入其他组件的组件,您可能应该做出一些折衷,以便在测试中使用轻型CDI层。

最好的解决方案是使用Arquillian和一个轻型CDI容器,如 arquillian-weld-ee-embedded-1.1 。使用CDI,您还可以使用@Alternative来提供bean的模拟版本。使用Arquillian,您的测试将如下所示:

@RunWith(Arquillian.class)
public class FooTest {
    @Inject
    Foo myFoo;


    @Deployment
    public static Archive<?> createTestArchive() throws FileNotFoundException {
        File beanFile = new File("src/test/resources/META-INF/beans.xml");
        if (!beanFile.exists())
            throw new FileNotFoundException();
        Archive<?> ret = ShrinkWrap
                .create(WebArchive.class, "test.war")
                .addClasses(Foo.class, FooImpl.class);
        return ret;
    }



    @Test
    public void testFoo() {
        // Do the test with Foo
    }
}

@Deployment静态方法创建项目的伪部署以启用CDI容器。您可以决定Maven文件中的容器类型,如

<profile>
        <id>weld-ee-embedded-1.1</id>
        <activation>
            <property>
                <name>arquillian</name>
                <value>weld-ee-embedded</value>
            </property>
        </activation>
        <dependencies>
            <dependency>
                <groupId>org.jboss.arquillian.container</groupId>
                <artifactId>arquillian-weld-ee-embedded-1.1</artifactId>
                <version>1.0.0.Alpha5</version>
            </dependency>
            <dependency>
                <groupId>org.jboss.weld</groupId>
                <artifactId>weld-core</artifactId>
            </dependency>
            <dependency>
                <groupId>org.jboss.weld</groupId>
                <artifactId>weld-api</artifactId>
            </dependency>
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-simple</artifactId>
            </dependency>
        </dependencies>
        <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>org.jboss.weld</groupId>
                    <artifactId>weld-core-bom</artifactId>
                    <version>1.1.0.Final</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
            </dependencies>
        </dependencyManagement>
    </profile>

您的测试不会进行集成测试,因为您只需要包装所需的内容并使用@Alternatives模拟其他组件,但您将拥有一个真正的CDI组件,其中包含容器和生命周期。