Search code examples
swiftunit-testingbddguard-statement

How to Get Test Coverage for Guard Statement Fall-through


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?


Solution

  • Just write whatever JSON you want — malformed in every way you can think of. Examples:

    • You can hit your exception-handling with something that isn't correct JSON.
    • You can hit your very first 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/