-3

Well consider a relative simple server for a SPA application. Written in nodejs with express and knex as backends.

Now if we do it properly each function will have unit tests (as they are always all visible to the end user). But besides of the choice of what function to create unit test for, is abstracting away the database actually worth the effort?

A simple function might look like:

function doSomething(request_object, response_object) {
    request_data = analyze_request();
    db_data = grabFromDatabase(request_data); //this is often just a knex query.
    result = reformatDBQuery(db_data);
    response_object.give_response(result);
}

Now to do it properly one would create mocks/stubs that either mock knex library. Or mock the interface methods we use ourselves. Both cases require a lot of writing, often so much as almost recreating the library ourselves.

The other option is to create only stubs for the exact function arguments required: but this would make tests quite brittle and the most common error I experience (by a large margin) wouldn't really be catched with this: Unexpected argument values in some function that do work but give nonsensical results that fail at another place.

So a tertiary option is possible: actually just use a database for testing. However this means the unit test is no longer a unit test. And it's really just an integration test. As this is about the only "complexity" that isn't trivial code it hence makes little sense to even have unit tests (the other tests are so trivial they can easily be added to the integration tests).

Especially since "launching" a real database connection in javascript is quite fast anyways. And writing mocks for every possible function in a library very, very time consuming.

So what do others do, especially with regards to "simple servers" where complexity is only found in database calls?

EDIT, to clarify, I understand the need for a unit test above integration tests. I wonder however what people do in practice given time constraints typically visible. And wonder how in reality people sidestep the problem of very very time consuming mocks.

EDIT2: to give some practicality:

Say I have a function that has as purpose "insert a new line with current session user id + some other data into a table". Which would look like:

async function createSheet(uId: number, data: number) {
    await express.locals.dbObject.insert({user: uId, data: data}).into("sheets");
}

Now a mocking library (I found) would do something like:

expect(knexDB).toHaveQueried(<exact query string here>).

But that query string (result from knex' query builder) will insert the data objects in "some" order generating a string, but it means I have to check into the implementation of knex itself in which order - very much an implementation detail.

While I'm basically only interested that I have inserted "something" for a given user-id, and that I can retrieve it later. -- So a proper "mock" would not only create stubs and let me query the exact query results, I'd also create an in memory database mimicking all functionality. So the tests can be descriptive.

  • 1
    Does this answer your question? Do I need unit test if I already have integration test? – gnat Jun 13 at 19:44
  • 1
    @jonrsharpe Well this is just "simplified" - but the other 2 layers are actually handled by the framework and have quite literary often near zero (non generated from settings files) code. Indeed those can easily be tested if it needs to be done, and are done (hence I name them as functions). - it's the middle function that I seem to be unable to think of a clear test approach. -- And this is also where I have least confidence anyways. – paul23 Jun 13 at 20:35
  • 1
    "The other option is to create only stubs for the exact function arguments required" - that's exactly what you should do in a single test; you set up the exact conditions and test one thing only. The idea is to pinpoint a specific constraint/invariant and to make the test reproducible. "But [...] Unexpected argument values in some function that do work but give nonsensical results that fail at another place" - that means you have coupling that you're not controlling, that you have hidden/implicit dependencies you're not aware of, and that you are missing tests. Fix that. – Filip Milovanović Jun 13 at 21:33
  • 1
    @paul23 - well, for what it's worth, the fact that you feel that way is another indicator that you (and your team, if this is a team effort) should do something about the design. Mistakes happen, yes, but spaghetti code happens when you keep doing the same thing for too long (and that same thing is often the "best practices" approach that initially got the project off the ground). – Filip Milovanović Jun 13 at 22:19
  • 1
    Tests are a consumer of your code; if they become a pain when you "change the input", then that means that any code that uses the SUT (or rather, that uses its core components) is also likely to cause you pain when you need to make changes (do you frequently have changes cascading through several layers, and it all seems more complicated than it should be?). What that really means is that your system resists change in its current state, and that you should have some plan on how to incrementally move out of that before it gets worse. – Filip Milovanović Jun 13 at 22:20
1

The usual requirements for using a DBMS in automated testing are

  • isolated testing by different developers, each one with their local db

  • individual, stable test data for each test

  • easy to maintain test data

  • easy to maintain data for the expected outcome of a test, especially in case the system evolves and the data has to adapted to changed requirements

  • test data which can be versioned in your SCCS

  • spinning up the database and a connection quickly, regardless if one runs only one test or hundreds

  • and of course, certain performance requirements, even if there are several hundred tests.

To my experience, fulfilling all these requirements is possible when one can use a very lightweight DB, ideally a single-file DB like SQLite. That lets you start with an individual defined test db for each test (just copy a template DB file into a working folder), connect instantly and run the tests pretty fast (usually much faster than on a heavyweight multi-user DB like Postgresql or MySql).

Using this approach can indeed give you "the most bang for the buck", and it can sometimes be more efficient than completely abstracting all database calls away. If it really is in your specific situation is something you have to measure by yourself, there is no hard-and-fast rule which approach is "better".

If you cannot go that route, since SQLite is not powerful or compatible enough for your case, you can try to get the requirements fulfilled with some Client/Server DBMS, but this will usually require a higher effort, and you need to try out (or estimate) if you think the effort is worth it in your case.

| improve this answer | |
  • 4
    To be honest, I've never been sold on this idea. I wouldn't use a SQLite database for testing unless the production database is also SQLite. What would the point of using two different databases be, unless you were using some subset of SQL that is compatible with both databases (something that rarely, if ever, happens), or you were testing cross-database compatibility? Use the same database for both testing and production, or don't bother. – Robert Harvey Jun 13 at 23:38
  • @RobertHarvey: I don't know Knex and how well it abstracts the differences between SQL dialects away. It will most probably depend heavily on how the OP uses the DB. But you have point. – Doc Brown Jun 14 at 7:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.