Having a component that represents the database is wonderful! You can use it from the business logic to read and write data. But should that component be unit-tested?

I would argue that it should not. Unit-tests are all about testing the contract of a component with its external actors.

Let's take a typical interface of a database component

void insertUsers(List<UserRecord> users);
List<UserRecord> fetchUsers(List<UserID> userIds);
void deleteUsers(List<UserID> userIds);

Each method listed utilizes the database library to send the appropriate queries to the database.

The external actors of the database component are:

  • The business logic
  • The database

Unit testing would require me to mock the connection, prepared statement and result set objects of the database library.

However since the entire database component consists only of interactions with the database library, any test I write will end up mirroring the component code and is hence fragile.

Testing the contract means asserting on the following:

  • Calling insertUsers should write those users in the database
  • Calling fetchUsers should retrieve those users from the database
  • Calling deleteUsers should delete those users from the database

Testing interaction with the database library will lead to fragile code:

  • A lot of equivalent SQL statements can be created. Changing a statement to an equivalent one does not break the contract and should not break the test
  • One could use the database library in different ways: eg: use a statement instead of a prepared statement object. That again does not break the contract
  • Changing the order of columns in a select or insert statement would yield equivalent results. No point in asserting on resultset.getString(0) or preparedStatement.setString(1, "Bill")
  • The database library used should not matter.

The following thoughts, have lead me to the conclusion that unit-tests for the database component offer little value. I truly feel that an integration test which requires an actual database is the way to go.

Please share your thoughts on the subject; Could it be that I am missing something?

Edit: Please suggest how can the following code be unit-tested in a non-fragile way. Feel free to refactor the code if you like.

void insertUsers(List<UserRecord> users) throws RepositoryException{
    try(Connection connection = datasource.getConnection();
        PreparedStatement stmt = connection.prepareStatement("insert into users (Name, Surname, DateOfBirth) values (?,?,?)")){
        for (UserRecord user : users){
            stmt.setString(1, user.name);
            stmt.setString(2, user.surname);
            stmt.setTimestamp(3, user.dateOfBirth);
            stmt.execute();
        }
    } catch(SQLException ex){
        throw new RepositoryException(ex);
    }
}
有帮助吗?

解决方案

Presumably, this database component is only a fairly thin wrapper that performs a translation between the objects used in the business logic and the SQL interface that the actual database provides.

You are correct that testing such a component in complete isolation is going to be fragile and not very useful. On the other hand, testing such a component in combination with an actual database does provide lots of added value.

If you can prove that the "database component" correctly interacts with an actual database for all the methods that the component provides, then you can run all the dozens business cases where a user gets retrieved with a mock of the database component and still have confidence that it will also work with an actual database.

其他提示

You are talking about a mock database, right? In my view, if a component used only for testing is worth having, then it's worth testing.

It doesn't matter if it's small and supports only a few of the things that an actual database supports. You should have test coverage of any non-trivial component in your code base, whether it lives in the business code or in the test suite. Naturally, if you can get everything under test without major helper classes, so much the better. But if using nontrivial helper classes in your test suite is the most cost-effective solution, then it can be worth the cost of writing additional tests for a mock class.

I agree with your thought process here. Integration tests are where you'll see the greatest benefit. The only place I would consider unit tests would be when you are cleaning up data prior to hitting the database. That will help to ensure you are sending what you expect and let you know if regressions are due to your code or the database's behavior. Integration tests will likely get you to the source of the problem quick enough though, so it's really a judgment call based on your code.

I will add that another nice benefit of the integration tests is that it will be easier for you to ensure consistent behavior when you go to upgrade the database. The newer version may have some syntax changes that unit tests would hide.

should that component be unit-tested?

Here's what Kent Beck wrote in 2008

I get paid for code that works, not for tests, so my philosophy is to test as little as possible to reach a given level of confidence

One of the things I find interesting about isolated tests, is that they encourage you to think about what parts of your code live in the functional core, and which interact with the imperative shell

Unit testing would require me to mock the connection, prepared statement and result set objects of the database library.

Maybe - but that's not all that is going on here. There are actually a number of different stages

  • Translate the representation of data from the local process into a representation of data for the database (ie, converting the data into a "query")
  • Send the query to the database
  • Receive the reply from the database
  • Translate the representation of data from the reply into a representation understood by the local system.

The translations are purely functional pieces that you could test with appropriate factoring of your elements.

Does your database component choose the right prepared statement? Does it put the correct arguments into the correct parameters? Do you translate result sets back into your domain objects correctly? Do you correctly handle the error conditions that might arise? All of those things are testable without leaving the process boundary.

Should you design your database component such that the functional parts can be tested? Probably - that's the "separation of concerns" heuristic at work.

Having separated the functional parts, should you test them? I like to borrow a heuristic from Hoare -- does the code obviously have no deficiencies? If that's true, then maybe it doesn't need tests. I'd guess that it does

Do you need to test the collaboration of the functional parts? Here, I would guess that you can write the collaboration in a way that it obviously has no deficiencies. I'd probably be comfortable even if this code was only tested at a system level.

Testing interaction with the database library will lead to fragile code

That's an overbid. Tightly coupling your tests to a particular implementation choice may lead to fragile tests. This is especially the case if your test interferes with non functional requirements.

It's a lot easier to delete a test that isn't useful than it is to run a test that isn't there. There is a real trade off to consider, but don't exaggerate the costs of end-of-lifing a test.

许可以下: CC-BY-SA归因
scroll top