On Mocking Rust

21 May 2018

Rust Rant Potboiler 

Originally posted at https://tech.labs.oliverwyman.com/blog/2018/05/21/on-mocking-rust/

I’ve recently picked Potboiler back up again with the intention of doing some expansion work on it, but in order to be able to do this sensibly I first had to actually write some tests for it. First time around I was mostly just trying to hack it together and see where I could get, but 19 months on I’d like to have a better structure under my feet. This turns out to be a lot harder than I’d expected. A lot of Potboiler is network-orientated, both around an HTTP client for messages between areas, and (currently) Postgres for local storage, and trying to make mock/fake versions of those in Rust is actually really quite hard. There’s a couple of decent mocking libraries around (double, galvanic-mock), but they’re all based around making mock versions of a trait, not structures. This is great if there’s a core trait that you need, but often there’s not.

Take for example Postgres. The core thing you’d want to mock is Connection, which you can’t, because it’s a struct. There’s the GenericConnection trait but that returns Rows and Statement structs that you’ve got no access to the internal fields so good luck making test ones of those. Ok, let’s try something higher level. r2d2 looked like a good candidate, being a generic connection pool, but then you hit the problem that a r2d2 Pool is parameterised on a particular connection type, so I can’t make a “this is is either Postgres or my test type” pool without fun with wrapper objects (and even then there’s no existing r2d2 test backend), and r2d2 specifies about zero of the actual details of the use of a connection. Next candidate: diesel, which in theory should be able to let me make a ‘wrapper connection’ that’s either Postgres or a test one right? Well, I went a long way down that route before finding out that at the bottom of the Backend trait is byteorder which has an 8 month old bug being heavily discussed about runtime byteorders, and so the ‘wrapper’ would have to support one byte ordering or the other, which isn’t really flexible enough.

HTTP is similarly fun. The only real game in town is Hyper which doesn’t natively support any test options, but you can hand the Client a custom NetworkConnector (probably made using yup-hyper-mock) and do some of this. Of course, this only lets you filter responses on host/schema/port not anything in the rest of the path. If I upgrade everything to 0.11 (which is a massive set of changes) I could abuse the Service trait to make something. I’d basically be figuring out how to do all from scratch, which isn’t that appealing yet, but I may well end up doing it.

My suspicion is that one of the things causing this sort of pattern is that up until very recently returning a abstract object implementing a trait was rather kludgy, but now we’re got ‘impl Trait‘ in stable we might see more implementations that return abstract things and so this will be much more likely to be less awful. Meanwhile, I’ve built about half of a “generic db interface”. I’m not interested yet in splitting it out for wider use, just from a “that feels like a big maintenance headache” perspective (especially given my experiments with Deuterium failed and so I’m making raw SQL queries).

One of the other options I could have probably done here is split out the DB fiddling bits from the rest of Potboiler, and have a generic trait for those changes that I mock out instead. That might actually be rather useful further down the line if I eventually move away from Postgres, but right now if I split out the DB bits there wouldn’t be that much left and I really wanted to test the entire “you send a log message and this results in a specific SQL query” full path. Or I could have just given up entirely and run just integration tests against Postgres, but I felt there was lots of bug cases (e.g. wrong field in some place in SQL query) that would be much harder to check from with just that.

Previously: Bazel: Fast, Correct, Usable – choose two Next: PSA: Many ways to manage your Python dependencies