Unit testing diesel code


#1

EDIT: by diesel, I am referring to https://github.com/diesel-rs/diesel
EDIT: also provide concrete details, not general/vague advice – i.e. I’m looking for people who have actually unit tested diesel code

Hi,

With most of my Rust code, it’s easy to unit test. I try to make the function a pure function or modify limited state, then I can

  1. setup a few local vars
  2. run the function
  3. assert conditions on results

With diesel, a lot of the important code is of the form “it modified the global database in this way.” The problem I have with unit testing this is that there is often one global database, defined as DATABASE_URL in .env , and all the schemas / macros that diesel auto generates ends up being linked directly to this database.

This makes it difficult to do step 1 of unit testing – I can’t just “create a few local vars” – there’s one global datbase, and everything references this global database.

Given this, how do I unit test diesel code?


#2

You can use Connection::test_transaction to run your test code in a transaction that will never get commited. This way, all your tests run in isolation and the data they change doesn’t affect/isn’t observable to the other tests.

(For questions like this I can recommend you to ask in https://gitter.im/diesel-rs/diesel directly – I’m not sure many people working on/with diesel read this forum, I just came across your thread by chance :slight_smile: )


#3

First, its not unit testing, its integration testing because it involves integration with foreign code (database).
The solution for that is to have test database set up by test runner - that’s what Django does. Each test (or test group) runs with it own database set up, lods its fixtures, and can verify database content after running.

Alternatively you could try to mock out the db - some fake diesel driver that does nothing and returns empty results might be what you are looking for. I know that testing diesel itself involves setting up db, so they probably don’t have something like that, but I might be wrong. That would be question to diesel developers.


#4

You’re right about this being integration testing; sorry, I should’ve pointed this out, too! I would recommend you do this, though, because AFAIK it is easier than mocking out diesel – at least at the moment.

Diesel’s internal tests have some short helper methods that open a test_transaction, run migrations, and add some ficture data. There is no “mock connection” right now, though it might be interesting to explore what would be possible with one. My gut feeling is that I’d try to adjust the testing architecture at a higher level; i.e., abstract over diesel, and make this new abstraction mockable.


#5

@killercup

Re: Connection::test_transaction suggestion.

My understanding is that this involves running a transaction against the live database, then rolling back instead of committing at the last second.

My question here is: do we have any control over the database state, or is it just “whatever state the live database is currently in?”

In particular, “whatever state the live database is currently in” may not satisfy the pre-conditions of our transaction.

===

@Fiedzia , @killercup

  1. I agree this looks like integration testing. However, if possible, I would like to view this as testing a function whose type involves a State Monad, where the State is a Datbase.

  2. It seems that if we use sqlite3’s “in memory mode” to create an empty db, populate it with some data, run the transaction, then make assertions, it’s almost like testing a pure function.

  3. I realize this doesn’t work with MySQL / Postgresql / … , but if my transactions work in Sqlite3, there’s a good chance they’re correct.

  4. One current approach for unit testing a diesel transaction is:

4a. create empty sqlite3 in memory db, run the diesel migrations (sets up the tables)

4b. run some sql-inserts to setup the pre-conditions we want

4c. run the diesel transaction

4d. assert facts about the database


#6

“Live” database? Run the same DBMS you use in production on your dev/test machine. Create an empty database in it, connect to it in tests.

I would always start from scratch. Each test would start with connecting to the test db, calling test_transaction, and then applying migrations (doesn’t work in MySQL, though).

This is basically what you described in 4, except 4a connects to the test db, and 4c comes after 4a directly.


Please note that Diesel right now is not focused on being able to use different DBMSs for the same query. If you want to use sqlite in tests, you should also use it in production, or introduce the necessary abstractions yourself.


#7

@killercup : My use of “live database” above added unnecessary confusion. I am prototyping a small kanban board in diesel for personal use, so “dev dv” and “live db” turned out to be same db. I see what you mean now, unit tests only run against dev db, and never touch live db.


#8

This might be a horribly ugly way of doing things, but it has worked pretty well for me.

I set up a separate test database which all the tests use. Then I use a lazy_static mutex containing the database connection which is shared by all the tests. When a test runs, it locks the mutex, resets the database and runs its operations.

This is the code for doing that: https://github.com/TheZoq2/flash/blob/master/src/file_database.rs#L382

I also have a macro that creates a #[test] function for each test that takes care of locking and reseting the database https://github.com/TheZoq2/flash/blob/master/src/test_macros.rs#L18

There are obviously downsides to this. All database tests run in series rather than in parallel. Also, since the database is in a mutex, any failing assert causes a panic which poisons the mutex meaning that all subsequent tests fail if one of them fails. I vaguely recall there being an alternative mutex in a crate somewhere which allows you to ‘unpoison’ a poisoned mutex which would be useful in this case.


#9

@TheZoq2 : Thanks for sharing you code. This is very helpful!
I am looking at the “reset” function – I see that it deletes the databases, but it does not recreate them or rerun the migrations.

Is there a function in your code which does the same as “diesel database reset” (but does it on the test db)?

[I think this is very close to what I need]


#10

I’m pretty sure that diesel::delete runs a DELETE query not a DROP query which removes the content of the table but keeps the table intact. That’s the way it’s behaving in my case at least


#11

This is what I have. A helper function like

pub fn with_db<F>(f: F) -> ()
where
	F: Fn(&PgConnection) -> (),
{
	let conn = db::get_test_connection();

	conn.test_transaction::<_, Error, _>(|| {
		f(&conn);
		Ok(())
	});
}

db::get_test_connection returns a connection to the test db.

Then my tests

fn it_does_something() {
	tests::with_db(|conn| {
		...
	})
}

#12

@TheZoq2 : Good call. This is my fault for confusing the SQL DROP/DELETE terms (first time taking SQL seriously). Thanks for clarifying!