-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.

paul23
  • 1,061
  • 1
  • 8
  • 14
  • 1
    Does this answer your question? [Do I need unit test if I already have integration test?](https://softwareengineering.stackexchange.com/questions/204786/do-i-need-unit-test-if-i-already-have-integration-test) – gnat Jun 13 '20 at 19:44
  • @gnat not really, that answers why the need is there - I understand that. But I'm wondering how to bring that "idea" into practice with the limited time constraints that are in place in reality. Maybe the title is misleading, but I wish to focus on the "effort" part. – paul23 Jun 13 '20 at 19:47
  • What you're seeing is a problem with your app architecture - that method covers ~three different abstraction levels (transport, storage, business). If your app logic was a distinct service layer, for the things where an integration test alone wasn't giving you sufficient confidence, you'd only have to mock simple things you owned (repository layer) and wouldn't have the controller layer creeping in. – jonrsharpe Jun 13 '20 at 20:13
  • 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 '20 at 20:35
  • What database(s) are you using? Knex seems to support Postgres, MSSQL, MySQL, MariaDB, SQLite3, Oracle, and Amazon Redshift. – Doc Brown Jun 13 '20 at 21:09
  • @DocBrown postgres, but we're writing db-technology agnostic code (apart from expecting some form of sql) so we can switch between myscql and postgresql easily. – paul23 Jun 13 '20 at 21:14
  • @paul23: what about SQLite? – Doc Brown Jun 13 '20 at 21:16
  • @DocBrown I'm not sure how that matters for this question - but right now that's outside the scope of the projects as other tools have less ability on sqlite and the benefits are non minimal in a container world. – paul23 Jun 13 '20 at 21:17
  • 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 '20 at 21:33
  • @FilipMilovanović sure we would love to do that, but mistakes happen. But the more fundamental problem is that now the test would immediatelly change whenever you change the input. A new test would have to be written as you can't just generalize the inputs (lest you spent a lot of time creating mocks). – paul23 Jun 13 '20 at 21:45
  • 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 '20 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 '20 at 22:20
  • P.S. About your specific question - I'm board with Doc Brown's answer regarding a lightweight database; this could also help you set up a safety net so that you can do refactoring and tweak the design to move towards some goal with more confidence. What I want to say here, though, is that the distinction between unit and integration tests isn't that relevant, what's more important is fast-to-run (unit or integr.) "developer tests" vs more costly higher-level tests. That's worth separating because you want to run the latter less frequently; see: https://martinfowler.com/bliki/TestPyramid.html – Filip Milovanović Jun 13 '20 at 22:29
  • @FilipMilovanović Well the problem is also similar to why we don't test private methods. They are implementation details. When one creates mocks you have to know exactly what the implementation is (and that of all private methods), so that you mock everything that is required. IE: does my functions only use find queries? Or also update? - Do they check the user on the database before updating or not? Do they only update the fields or also make a log-update somewhere else? While one could argue that these are all visible outputs, things like databases are so complex it's hard to envision every. – paul23 Jun 18 '20 at 09:53

1 Answers1

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.

Doc Brown
  • 199,015
  • 33
  • 367
  • 565
  • 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 '20 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 '20 at 07:40