【问题标题】:Junit Mockito test everythingJunit Mockito 测试一切
【发布时间】:2014-10-20 15:27:21
【问题描述】:

我现在正在寻找更多小时,但没有结果。请帮忙...

这是我要测试的课程:

public class DBSelectSchema extends Database {

    private static final Logger LOG = Logger
            .getLogger(DBSelectSchema.class.getName());
    private Connection conn = null;

    public DBSelectSchema() {
        super();
    }

    /**
     * This method will return the version of the database.
     * 
     * @return version
     * @throws Exception
     */
    public JSONObject getVersionFromDB() throws SQLException {
        ResultSet rs = null;
        JSONObject version = new JSONObject();
        PreparedStatement query = null;

        try {
            conn = mensaDB();
            query = conn.prepareStatement("SELECT number FROM version");

            rs = query.executeQuery();

            if (rs.isBeforeFirst()) {
                rs.next();
                version.put(HTTP.HTTP, HTTP.OK);
                version.put("version", rs.getString("number"));
            } else {
                version.put(HTTP.HTTP, HTTP.NO_CONTENT);
                version.put(HTTP.ERROR, "Die SQL Abfrage lieferte kein Result!");
            }

            rs.close();
            query.close();
            conn.close();

        } catch (SQLException sqlError) {
            String message = ERROR.SQL_EXCEPTION;
            LOG.log(Level.SEVERE, message, sqlError);
            return version;

        } catch (JSONException jsonError) {
            String message = ERROR.JSON_EXCEPTION;
            LOG.log(Level.SEVERE, message, jsonError);
            return version;
        }

        return version;
    }

我试图进入每个分支以实现 100% 的代码覆盖率。 如何模拟 ResultSet rs、JSONObject 版本和 PreparedStatement 查询来执行/返回我想要的:

目前我正在这样测试:

@Test
    public void getVersionFromDB_RS_FALSE() throws SQLException, JSONException {
        MockitoAnnotations.initMocks(this);

        Mockito.when(dbSelMocked.mensaDB()).thenReturn(conn);
        Mockito.when(conn.prepareStatement(Mockito.anyString())).thenReturn(query);
        Mockito.when(query.executeQuery()).thenReturn(rs);
        Mockito.when(rs.isBeforeFirst()).thenReturn(false);

        JSONObject returnObj = dbSelMocked.getVersionFromDB();

        assert(...);
    }

但这仅适用于 3 个变量是类变量(如 Connection conn)而不是局部变量时。但我不希望它们(甚至连接)不是全球性的。

=== 编辑 1 ===

如果所有变量都是本地变量,它的工作方式如下:

@Test
    public void getVersionFromDB_RS_FALSE() throws SQLException, JSONException {
        System.out.println("####################");
        System.out.println("started test: getVersionFromDB_RS_FALSE");
        System.out.println("####################");

        Connection conn = Mockito.mock(Connection.class);
        PreparedStatement query = Mockito.mock(PreparedStatement.class);
        ResultSet rs = Mockito.mock(ResultSet.class);

        MockitoAnnotations.initMocks(this);


        Mockito.when(dbSelMocked.mensaDB()).thenReturn(conn);
        Mockito.when(conn.prepareStatement(Mockito.anyString())).thenReturn(query);
        Mockito.when(query.executeQuery()).thenReturn(rs);
        Mockito.when(rs.isBeforeFirst()).thenReturn(false);

        JSONObject returnObj = dbSelMocked.getVersionFromDB();

        assertTrue(returnObj.has("error"));
    }

但是我不能再在另一个测试中模拟 JSONObject 版本了:( 我该怎么做?

@Test
    public void getVersionFromDB_JSON_EXCEPTION() throws SQLException, JSONException {
        System.out.println("####################");
        System.out.println("started test: getVersionFromDB_JSON_EXCEPTION");
        System.out.println("####################");
        JSONObject version = Mockito.mock(JSONObject.class);

        MockitoAnnotations.initMocks(this);

        doThrow(new JSONException("DBSelectSchemaIT THROWS JSONException")).when(version).put(anyString(), any());

        JSONObject returnObj = dbSelMocked.getVersionFromDB();

        System.out.println(returnObj.toString());

        assertTrue(returnObj.equals(null));
    }

我认为它在真正的方法中被覆盖了......因为它不会抛出异常并且方法不会失败。

【问题讨论】:

  • 追求 100% 的代码覆盖率通常是完全浪费时间。根据类的要求编写测试,而不是根据组成它的代码行。

标签: java testing junit mockito code-coverage


【解决方案1】:

你的测试太大了,你似乎测试太多了。

将您的代码沿其自然中断拆分,以便进行数据检索的代码与操作它的逻辑分开。

您仅想测试您编写的代码,而不是第 3 方代码。它超出了您的需求范围,如果您不能信任它,请不要使用它。

【讨论】:

    【解决方案2】:

    您的测试代码有多个问题。

    • 测试冗长而脆弱
    • 多个测试需要相同(详细)设置
    • 您不测试真实对象,而是使用模拟类进行测试

    前两个问题可以通过将重复代码提取到设置方法来解决(我为 Mockito 添加了静态导入以减少噪音):

    @Before
    public void setUp() throws Exception {
        Connection conn = mock(Connection.class);
        PreparedStatement query = mock(PreparedStatement.class);
        when(dbSelMocked.mensaDB()).thenReturn(conn);
        when(conn.prepareStatement(anyString())).thenReturn(query);
        when(query.executeQuery()).thenReturn(rs);
    
        rs = mock(ResultSet.class); // rs is field
    }
    

    现在在您的每个测试中,您都可以配置 rs 以返回您需要的任何内容:

    @Test
    public void getVersionFromDB_RS_FALSE() throws Exception {
        // Given
        when(rs.isBeforeFirst()).thenReturn(false);
    
        // When
        JSONObject returnObj = dbSelMocked.getVersionFromDB();
    
        // Then
        assertTrue(returnObj.has("error"));
    }
    

    现在最重要的问题是:您正在模拟类 DBSelectSchema 以返回连接模拟。被测试的模拟类可能会导致不同的难以发现的问题。

    要解决这个问题,您有 3 个选项:

    1. 重构您的代码并注入一些连接工厂。所以你会 能够在您的测试中模拟它。

    2. 在您的测试和覆盖方法中扩展类 DBSelectSchema mensaDB() 所以它会返回模拟连接

    3. 使用像 H2 这样的嵌入式数据库并将测试数据放在“数字”表中 在调用 getVersionFromDB() 之前

    选项 #1

    将创建的连接提取到一个单独的类并在您的DBSelectSchema 中使用它:

    public class ConnectionFactory {
        public Connection getConnection() {
           // here goes implementation of mensaDB()
        }
    }
    

    然后将其注入您的 DBSelectSchema:

    public DBSelectSchema(ConnectionFactory connFactory) {
        this.connFactory = connFactory;
    }
    

    现在您的测试可以使用 real DBSelectSchema 类和 mocked ConnectionFactory

        ConnectionFactory connFactory = mock(ConnectionFactory.class);
        dbSel = new DBSelectSchema(connFactory); 
    

    选项 #2

    你可以让几乎真正的被测试类:

        final Connection conn = mock(Connection.class);
        dbSel = new DBSelectSchema() {
            @Override
            public Connection mensaDB() {
                return conn;
            }
        }; 
    

    选项#3

    这个选项是最可取的,因为您将调用真正的 SQL 命令并模拟整个数据库而不是类。在这里使用纯 JDBC 需要付出一些努力,但这是值得的。请记住,SQL 方言可能与生产中使用的数据库不同。

    @Before
    public void setUp() throws Exception {
        Class.forName("org.h2.Driver");
        conn = DriverManager.getConnection("jdbc:h2:mem:test;INIT=RUNSCRIPT FROM 'classpath:schema.sql'");
    }
    
    @After
    public void tearDown() throws Exception {
        conn.close();
    }
    

    然后在您的测试中,您只需将所需的记录添加到 DB:

     @Test
     public void getVersionFromDB() throws Exception {
        // Given
        conn.prepareStatement("INSERT INTO version(number) VALUES (1)").execute();
    
        // When
        JSONObject returnObj = dbSel.getVersionFromDB();
    
        // Then
        assert(...);
    }
    

    显然,DBSelectSchema 必须使用相同的连接,所以可以结合选项#1 和#2 使用,

    【讨论】:

    • 谢谢。我对数据库应用程序没有太多经验。 1)你能举一个注入连接工厂的例子吗?
    • 我添加了示例和更多解释。希望对你有帮助
    • 我会试一试的。非常感谢您的宝贵时间!
    【解决方案3】:

    您正在通过模拟所有 ADO 调用对数据访问层进行单元测试。通过这样做,您最终会得到一个不会真正测试任何逻辑的单元测试。

    以您的代码为例:假设您正在使用以下 sql 检索版本号:SELECT number FROM version。现在假设列名已更改,您应该从 sql 中检索 2 个附加列。你最终会得到一个像SELECT number, newColumn1, newColumn2 FROM version 这样的sql。使用您编写的测试(使用模拟),即使它没有真正测试是否正在检索 2 个新列,它仍然会通过。你明白我的意思吗?

    我建议您查看thread 以了解一些可能的替代方法来测试您的数据访问层。对数据访问层使用 mock 最终会导致无法真正测试任何东西的脆弱测试

    【讨论】:

      猜你喜欢
      • 2020-09-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-12-20
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多