Testing is a critical part of software development, ensuring functionality, reliability, and maintainability. With AI tools like Aider Chat, automating the creation of unit tests becomes faster and less repetitive. In this post, I explore using Aider Chat to ‘semi-automate’ the writing of unit tests for an iOS project, highlighting its strengths, pitfalls, and strategies for success.
Installation is straightforward through the official Aider documentation. Before diving in, set up conventions to guide Aider’s behavior, such as an AI_CONVENTIONS.md
file. This file can define consistent patterns, naming conventions, and dependency strategies to improve the quality of generated tests.
After writing ~ 6 tests, this is what my conventions file looked like, I expect this to grow over time
- Unit tests
- When a mock already exists for a protocol in another file prefer to use that Mock instead of create a new one.
- All new Mocks should go in a file '$(direrctory_name)Mocks' so we have a single file for each group of tests
- When Asserting against enums that don't conform to Equatable prefer to use a pattern like 'guard case '.case(.subcase)' = .result else { fail } instead of making the enum implement Equatable
- When testing URLComponents, there is no guaranteed order for the URL Query params, test for the existence of the parameter and the value, not asserting against the whole string
- Each unit test should instantiate an instance of the entity being tested, and name this variable `sut`
Conventions can sometimes be specific to a certain domain or general across all tests. Aider has the facility to specify multiple conventions files allowing the flexibility to have a set of ‘common’ conventions, and then more specific conventions can be loaded for example when testing networking patterns, or local persistence.
A consistent dependency injection (DI) pattern is essential. In these tests I have used one of the most common and simple DI patterns which is to inject dependencies through the constructor, using protocols to abstract their interface from their implementation.
Here’s an example of using dependency injection to wrap a NetworkLayer (protocol) within a class, decorating the functionality to create a JSONDecodingNetworkLayer:
class JSONDecodingNetworkLayer: DecodingNetworkLayer {
private let wrapped: NetworkLayer
init(wrapping networkLayer: NetworkLayer) {
self.wrapped = networkLayer
}
// rest of functionality omitted for brevity
}
There are many of other valid and standardised patterns for dealing with dependencies for our code, the key thing is that there should be consistency across your project and that you are able to define your strategy concisely. This will significantly help your AI produce sensible output when writing tests.
While AI tools promise to streamline testing, they are not infallible. Your role as a developer includes:
To get the best results with Aider, I followed a structured workflow:
Using o1-preview, the most advanced reasoning model from Open AI I asked it to create unit tests for the struct: HeadersModifier.
struct HeadersModifier: NetworkingModifier {
private var headers: [String: String]
init(headers: [String: String]) {
self.headers = headers
}
func send<T: Decodable>(request: URLRequest, upstream: some DecodingNetworkLayer, decodeTo type: T.Type) async -> Result<T, NetworkIntegrationError> {
var modifiedRequest = request
for (key, value) in headers {
modifiedRequest.setValue(value, forHTTPHeaderField: key)
}
return await upstream.send(modifiedRequest, decodeTo: type)
}
}
HeadersModifier
to be instantiated injecting a range of constructor arguments for each test// Within Setup
mockUpstream = MockDecodingNetworkLayer()
networkLayer = mockUpstream.addHeaders(["Authorization": "Bearer token"])
// Within test...
// When
let result: Result<SampleData, NetworkIntegrationError> = await networkLayer.send(originalRequest, decodeTo: SampleData.self)
The entity being tested with networkLayer.send()
is actually of type MockDecodingNetworkLayer
, which doesn’t test the functionality of HeadersModifier
at all!
HeadersModifierTest
didn’t instantiate an instance of HeadersModifier
, so didn’t clearly test the functionality of our System Under Test (SUT). Expectations 1 and 3 are not being met.
The biggest improvement to the process to solve issues like this was to break down the code writing process into ‘baby steps’. In this case, these steps were:
I found that a lot of the understanding of ‘how to setup to test’ was developed using a free model such as “gemini/gemini-1.5-pro-latest”. Once I felt more confident in the setup, I moved to a paid model like ‘gpt-4o’ to improve the quality of the code completions.
For the end result in code, here is the Pull Request I merged with the tests generated using Aider Chat.
Model | System Under Test | Result | Conventions Used | Comments | Other Notes |
---|---|---|---|---|---|
gemini/gemini-1.5-pro-latest | JSONDecodingNetworkLayer | Didn’t compile; manually fixed | None | Covered test cases but required manual fixes. | Saved time on boilerplate. |
HeadersModifier | Compiled successfully | None | Duplicated the Mock of NetworkLayer, creating redundancy. | First test used as a prompt example. | |
IdentityModifier | Didn’t compile; manually fixed | None | Covered test cases; boilerplate time saved. | ||
IdentityModifier | Compiled successfully | AI_CONVENTIONS.md | Unexpected changes made to Identity.swift; changes respected conventions. | Added conventions file to improve consistency. | |
RequestBuilder | Compiled partially; 1 test failed | AI_CONVENTIONS.md | Issue with URL parameter order; revised test improved thoroughness. | Added incomplete code to RequestBuilderTests. | |
RequestBuilderDecodingEndpointNetwork | Didn’t compile; manually fixed enums | AI_CONVENTIONS.md | Struggled with comparing enum cases; manual fixes required using pattern matching. | ||
o1-preview | HeadersModifier | Crashed | AI_CONVENTIONS.md | HeadersModifier test didn’t instantiate the SUT, so the generated test was unusable. | Cost $0.15. |
gpt-4o | HeadersModifier | 1st pass didn’t compile; overcomplicated | AI_CONVENTIONS.md | HeadersModifier test unusable due to complexity and compilation issues. | |
gpt-4o split between mocks and tests | HeadersModifier, IdentityModifier, RequestBuilder, RequestBuilderDecodingEndpointNetwork | 🏆 All test cases compiled successfully on first try |
AI_CONVENTIONS.md | Tests improved by splitting instructions into mock generation and testing. | Reduced added files; minor manual syntax edits for guard case. |