This week, continuing my work on the Mastermind project, I implemented more automated testing! More specifically, I refactored my old testing code from integrated tests to unit tests, and finished implementing tests for other parts of the code.
Integration Test vs Unit Test
In the early stages I had implemented some testing. I even mentioned this in a previous blog. At the time I thought they were unit tests. After going through the doc, I learned they were actually integration tests, since I had them in a centralized separate module.
In the spirit of trying something different, I decided to migrate them to unit tests, where I keep the test code in the same place as the program code. It also gave me a change to compare these two approaches:
With my integration tests, my focus was mainly the input and output. I set up a mock server and ran all the necessary code to produce the final output. Finally, I used the assert_eq!
macro to ensure the output was as expected.
Migrating to unit tests, I had to break the test code into pieces and focus on one function in each test unit. I also noticed myself naturally think about validating every step rather than just the input and output.
Correct me if I'm wrong, but here are my thoughts: Integration testing works better for a top-down approach where you want to ensure all the possible inputs produce the correct outputs without failure. Unit testing works better for a bottom-up approach where you want to ensure each component of the code is working correctly.
Mock API Server and Config Files
The most challenging part was the code related to API calling and config files, since they require some kind of external resources which I had to simulate for these tests.
For the mock API server, I used the httpmock
crate. It allowed me to create an HTTP server for my testing environment. The server would simply serve a static file so the output is predictable.
For the config files, I used the 'tempfile' crate, or more specifically, the tempdir
function to create a temporary directory. So that any file creation or modification can happen inside without affecting the real config files.
To Set or Not to Set
I did run into a dilemma regarding struct member visibility. To manage API calls, I had a struct like so:
pub struct Instance {
client: reqwest::Client,
base_url: String,
api_key: String,
}
As you can see, the members are private. However, during testing I needed to change the base_url
to my local mock server.
On the one hand, I have recently read a discussion that idiomatically, Rust prefers public struct members to "getters" and "setters", since these functions can get in the way of Rust's borrow checker.
On the other hand, is it really a good idea to make it a public member just because it's needed for a test?
I ended up going with a setter, with a #[cfg(test)]
annotation so that it's only compiled for tests, like so:
#[cfg(test)]
impl Instance {
pub(crate) fn set_base_url(&mut self, base_url: String) {
self.base_url = base_url;
}
}
Still, this doesn't feel quite right. I wonder if there's a better solution out there?
Top comments (0)