【问题标题】:Testing an EJB with JUnit使用 JUnit 测试 EJB
【发布时间】:2011-09-22 02:55:38
【问题描述】:

我应该如何测试注入了 EntityManager 实例的 EJB 3.1?

一个可能的 EJB:

@Stateless
@LocalBean
public class CommentService {

    @PersistenceContext
    private EntityManager em;

    public List<Comment> findAll() {
        TypedQuery<Comment> query = em.createNamedQuery(
            Comment.FIND_ALL, Comment.class
        );
        return query.getResultList();
    }

}

一个可能的测试:

@Test
public void testFindAll() {
    List<Comment> all = service.findAll();
    Assert.assertEquals(8, all.size());
}

我只为 Java EE 开发人员使用 GlassFish 3.1 和 Eclipse Indigo。我已经尝试过这样的事情:

@Before
public void setUp() throws Exception {
    ejbContainer = EJBContainer.createEJBContainer();
    service = (CommentService) ejbContainer.getContext()
        .lookup("java:global/classes/CommentService");
}

但我得到的只是:

javax.ejb.EJBException:
No EJBContainer provider available: no provider names had been found.

【问题讨论】:

    标签: jpa jakarta-ee junit java-ee-6


    【解决方案1】:

    接受的答案需要模拟大量代码,包括持久层。改为使用嵌入式容器来测试实际的 bean;否则,模拟持久层会导致代码几乎无法测试任何有用的东西。

    将会话 bean 与引用持久性单元的实体管理器一起使用:

    @Stateless
    public class CommentService {
    
        @PersistenceContext(unitName = "pu")
        private EntityManager em;
    
        public void create(Comment t) {
            em.merge(t);
        }
    
        public Collection<Comment> getAll() {
            Query q = em.createNamedQuery("Comment.findAll");
            Collection<Comment> entities = q.getResultList();
            return entities;
        }
    }
    

    实体 bean:

    @Entity
    @NamedQueries({@NamedQuery(name = "Comment.findAll", query = "select e from Comment e")})
    public class Comment implements Serializable {
        private static final long serialVersionUID = 1L;
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Long id;
    
        public Long getId() {
            return id;
        }
    
        public void setId(Long id) {
            this.id = id;
        }
    }
    

    这个持久化单元在persistence.xml文件中定义如下:

    <persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
    
      <persistence-unit name="pu" transaction-type="JTA">
        <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
        <class>org.glassfish.embedded.tempconverter.Comment</class>
        <properties>
          <property name="eclipselink.ddl-generation" value="drop-and-create-tables"/>
        </properties>
      </persistence-unit>
    </persistence>
    

    交易类型必须是JTA

    然后编写一个创建和销毁 EJB 容器(GlassFish 嵌入式容器)的测试:

    public class CommentTest extends TestCase {
    
         private Context  ctx;
         private EJBContainer ejbContainer;
    
        @BeforeClass
        public  void setUp() {
            ejbContainer = EJBContainer.createEJBContainer();
            System.out.println("Opening the container" );
            ctx = ejbContainer.getContext();
        }
    
        @AfterClass
        public  void tearDown() {
            ejbContainer.close();
            System.out.println("Closing the container" );
        }
    
        public void testApp() throws NamingException {
    
            CommentService converter = (CommentService) ctx.lookup("java:global/classes/CommentService");
            assertNotNull(converter);
    
            Comment t = new Comment();
            converter.create(t);
            t = new Comment();
            converter.create(t);
            t = new Comment();
            converter.create(t);
            t = new Comment();
            converter.create(t);
    
            Collection<Comment> ts = converter.getAll();
    
            assertEquals(4, ts.size());
        }
    }
    

    接下来,添加两个依赖项(例如添加到 Maven POM):

    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.8.2</version>
        <scope>test</scope>
        <type>jar</type>
    </dependency>
    <dependency>
        <groupId>org.glassfish.main.extras</groupId>
        <artifactId>glassfish-embedded-all</artifactId>
        <version>3.1.2</version>
        <scope>compile</scope>
    </dependency>
    

    拥有 dependenciessessionentity bean、persistence 文件、测试 文件完全按照所示实现,那么测试应该通过。 (网上的例子严重不足。)

    【讨论】:

    • 100% 确认!此外,您还可以通过更改您的 Maven 依赖项来切换嵌入式 EJB 容器。我倾向于使用 OpenEJB,因为它在测试期间启动得更快,您甚至可以考虑在正常构建期间运行这种测试,因为它们不会花费很多时间。在此处查看一些示例:tomee.apache.org/examples-trunk
    • 虽然我现在意识到需要在测试阶段分离持久化单元。我仍在努力解决这个问题。
    • Definitley 这个答案超过了接受的答案。尽管被接受的可能是字面上正确的(单元测试与集成测试),但重要的是:我的代码是否符合我的预期。要知道这一点,您必须通过一些实际数据对其进行测试。模拟很好,很好,很容易,但它永远不会告诉你你建立了一个错误的复杂标准。这将。
    • 接受的答案正确地指出了单元测试和集成测试之间的区别。单元测试应该只测试很少。它们应该是一些非常具体的破损的迹象。在没有模拟依赖的容器中测试 bean 意味着引入大量关于被测类失败的误报的可能性。这不是您想要的单元测试。您的测试测试您的系统是否正常工作。万一出现故障,您需要开始挖掘,而不是有一个指向非常具体的错误的精确指针。
    • “单元测试和集成测试之间的界限也很模糊”我绝对同意这个说法,这两种测试必须分开,否则你的构建时间在您的开发过程中增加太多。系统集成测试也必须与那些 2 分开。
    【解决方案2】:

    可以编写针对容器运行的单元测试,但需要注意的是容器/应用服务器必须启动。由于这并不实际,因此一般方法是使用“模拟”容器来运行您的单元测试。为此,请查看 JUnitEE 或 ejb3unit:

    junitee

    ejb3unit

    【讨论】:

    • 警告:JUnitEEEJB3Unit 现在都已完全弃用且无用。使用Arquillian 或自己管理嵌入式容器。
    【解决方案3】:

    为什么不使用Arquillian 编写单元测试并在真实容器中运行它们!?

    没有更多的嘲笑。没有更多的容器生命周期和部署麻烦。只是真正的测试!

    模拟可以是战术性的,但更多时候,它们用于使代码在真实环境之外工作。 Arquillian 让您抛弃模拟并编写真正的测试。这是因为 Arquillian 将您的测试带到了运行时,让您可以访问容器资源、有意义的反馈以及对代码实际工作方式的洞察。

    更多关于Arquillian features.

    【讨论】:

    • 也许是因为 Arquillian 很慢,需要一个正在运行的容器?
    • 现在容器启动速度越来越快,您也可以使用正在运行的远程容器,然后无需为每个测试运行容器。
    • Arquillian 一点也不友好,我花了几个小时试图让测试运行 b/c 这并不是那么明显,或者很容易忘记添加类/包/库到收缩包装档案。或者我应该说 Shrinkwrap 文档写得不好。文档就是一切,尤其是 Java,因为有太多的选择和行话,以至于对于刚开始并决定尝试 Arquillian 却发现很难开始工作的人来说,这是非常压倒性的。
    • Arquillian 使用嵌入式 glassfish 和嵌入式数据库非常快。在测试 ejb 时,它的集成测试很重要
    • 对我来说,Arquilian 还没有达到它所提供的好处比它所需的设置节省更多时间的地步。我希望它很快就会实现,但现在,正确设置很痛苦。
    【解决方案4】:

    首先,确保区分单元测试集成测试。 JUnit 只是一个帮助您组织和运行测试的框架,但您必须确定测试的范围。

    我假设您有兴趣定义CommentService.findAll()单元 测试。那是什么意思?这意味着我将验证调用 findAll() 方法会导致 CommentService 调用由 FIND_ALL 字符串常量命名的命名查询。

    由于依赖注入和存根,您可以使用例如Mockito 存根 EntityManager。对于单元测试,我们只关注findAll() 中的业务逻辑,所以我也不会测试评论服务的查找——测试评论服务是否可以被查找并连接到适当的实体manager 实例在集成测试的范围内,而不是单元测试的范围内。

    public class MyCommentServiceUnitTest {
        CommentService commentService;
        EntityManager entityManager;
    
        @Before
        public void setUp() {
            commentService = new CommentService();
    
            entityManager = mock(EntityManager.class);
            commentService.setEm(entityManager); // inject our stubbed entity manager
        }
    
        @Test
        public void testFindAll() {
            // stub the entity manager to return a meaningful result when somebody asks
            // for the FIND_ALL named query
            Query query = mock(Query.class);
            when(entityManager.createNamedQuery(Comment.FIND_ALL, Comment.class)).thenReturn(query);
            // stub the query returned above to return a meaningful result when somebody
            // asks for the result list
            List<Comment> dummyResult = new LinkedList<Comment>();
            when(query.getResultList()).thenReturn(dummyResult);
    
            // let's call findAll() and see what it does
            List<Comment> result = commentService.findAll();
    
            // did it request the named query?
            verify(entityManager).createNamedQuery(Comment.FIND_ALL, Comment.class);
            // did it ask for the result list of the named query?
            verify(query).getResultList();
            // did it return the result list of the named query?
            assertSame(dummyResult, result);
    
            // success, it did all of the above!
        }
    }
    

    通过上面的单元测试,我测试了findAll() 实现的行为。单元测试验证获得了正确的命名查询,并且命名查询返回的结果返回给被调用者。

    更重要的是,上面的单元测试验证了findAll() 的实现是正确的,独立于底层JPA 提供者和底层数据。我不想测试 JPA 和 JPA 提供程序,除非我怀疑第 3 方代码中存在错误,因此删除这些依赖项可以让我将测试完全集中在 Comment 服务的业务逻辑上。

    可能需要一些时间来适应使用存根测试行为的心态,但它是测试 EJB 3.1 bean 的业务逻辑的一种非常强大的技术,因为它允许您隔离每个测试的范围并将其缩小到排除外部依赖。

    【讨论】:

    • i+1 但它强制你创建一个setter方法(setEm)。对我来说没关系,因为为了完全可测试,编写代码时应该考虑到可测试性。您还了解单元测试和集成测试之间的区别。这正是单元测试“EJB”的含义
    • 我不明白这应该如何测试他已将 8 个元素插入到他的对象列表中。这个测试还不够深入。
    • 我会坚持将测试类命名为Xxxx XxxxTest 的约定。在这种情况下,那将是 CommentServiceTest。断言似乎是不必要的,因为在验证了我们的模拟方法 getResultList() 已被调用之后,result 包含 dummyResult 是给定的。
    • OP 显然想要一个 real 测试,其中使用真正的EntityManager。在一个孤立的单元测试中模拟它没有抓住重点。
    • 同意,这个测试充其量是测试零业务逻辑。在最坏的情况下,这个测试给出了误报。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-01-19
    • 1970-01-01
    相关资源
    最近更新 更多