I started writing iOS unit tests today with the BDD approach. I have a question regarding guard
statements and getting to 100% code coverage.
I have the following code, which handles the conversion of Data
into Customer
objects.
internal final class func customer(from data: Data) -> Customer? {
do {
guard let jsonDictionary = try JSONSerialization.jsonObject(with: data, options: []) as? Dictionary<String, Any> else {
return nil
}
var customerFirstName: String? = nil
var customerLastName: String
if let firstName = jsonDictionary["first_name"] as? String {
customerFirstName = firstName
}
guard let lastName = jsonDictionary["last_name"] as? String else {
return nil
}
customerLastName = lastName
return Customer(firstName: customerFirstName, lastName: customerLastName)
} catch {
return nil
}
}
When our backend was created, some customers were given just a last name, which contained their first and last names. That is why the customer's first name is optional; their full name may be the value for last_name
.
In my code, the customer's first name is optional while their last name is required. If their last name is not returned in the received JSON from a network request, then I do not create the customer. Also, if the Data
cannot be serialized into a Dictionary
, then the customer is not created.
I have two JSON files, both of which contain customer information that I am using to test both scenarios.
One contains no first name in the JSON:
{
"first_name": null,
"last_name": "Test Name",
}
The other contains a first name in the JSON:
{
"first_name": "Test",
"last_name": "Name",
}
In my unit test, using Quick and Nimble, I handle the creation of a Customer
when the first name is not available and when it is:
override func spec() {
super.spec()
let bundle = Bundle(for: type(of: self))
describe("customer") {
context("whenAllDataAvailable") {
it("createsSuccessfully") {
let path = bundle.path(forResource: "CustomerValidFullName", ofType: "json", inDirectory: "ResponseStubs")!
let url = URL(fileURLWithPath: path)
let data = try! Data(contentsOf: url)
let customer = DataTransformer.customer(from: data)
expect(customer).toNot(beNil())
}
}
context("whenMissingLastName") {
it("createsUnsuccessfully") {
let path = bundle.path(forResource: "CustomerMissingLastName", ofType: "json", inDirectory: "ResponseStubs")!
let url = URL(fileURLWithPath: path)
let data = try! Data(contentsOf: url)
let customer = DataTransformer.customer(from: data)
expect(customer).to(beNil())
}
}
}
}
This ensures that I am creating a Customer
when the first name is missing or present in the returned JSON.
How can I get to 100% code coverage of this method, using BDD, when my code does not hit the else
clauses of the guard
statements since the data is able to be turned into valid JSON objects? Should I just add another .json
file with data that cannot be transformed into a JSON object to ensure that a Customer
is not created as well as a .json
file that contains a missing last_name
to ensure that a Customer
is not created?
Am I just over-thinking the "100% code coverage" concept? Do I even need to have the else
clauses of the guard
statements tested? Do I even have the appropriate approach using the BDD method?
Just write whatever JSON you want — malformed in every way you can think of. Examples:
guard
with something that is a JSON array, not a dictionary.As the saying goes, you only need to cover code that you want to be correct. 😉
TDD and BDD are related. In TDD, you'd write a failing test first. Then, you'd write code that passes that test as quickly as you can. Finally, you'd clean up your code to make it better. It looks like you're adding tests after-the-fact.
By the way, your tests would be much clearer if you didn't use external files, but put the JSON straight into your tests. Here's a screencast showing how I TDD the beginnings of JSON conversion. The screencast is in Objective-C but the principles are the same: https://qualitycoding.org/tdd-json-parsing/