Unit Test

Introductions:

1. XCTestCase and Equality Assertion

  • XCTAssert(_)
  • XCTAssertEqual(_, _)
  • XCTAssertNotEqual(_, _)
  • XCTAssertLessThan(_, _)
  • XCTAssertLessThanOrEqual(_, _)
  • XCTAssertGreaterThan(_, _)
  • XCTAssertGreaterThanOrEqual(_, _)
  • XCTAssertNil(_)
  • XCTAssertNotNil(_)
  • XCTAssertTrue(_)
  • XCTAssertFalse(_)
  • XCTAssertNoThrow(_)
  • XCTAssertThrowsError(_)
  • XCTUnwrap(_)
  • XCTestExpectation(description: _)

2. Life Cycle

class LifeCycleExampleTest: XCTestCase {
    
    override class func setUp() {
        print("This runs before all tests")
    }
    
    override func setUpWithError() throws {
        print("This runs before each test")
    }
    
    override func tearDownWithError() throws {
        print("This runs after each test")
    }
    
    override class func tearDown() {
        print("This runs after all tests")
    }
    
    func testA() throws {
        print("This is test A")
    }
    
    func testB() throws {
        print("This is test B")
    }
}

3. Simple Example

    func divideNumber(_ number: Int) -> String {
        switch (number % 3 == 0, number % 5 == 0){
        case (false, false): return "not_divide"
        case (true, false): return "divide_by_3"
        case (false, true): return "divide_by_5"
        case (true, true): return "divibe_by_both_3_5"
        }
    }
    
    func test(value: String, expected matches: String) {
        print(value == matches ? "\(value) ✅PASSED" : "\(value) FAILED❌")
    }
    
    func testDivideNumber(){
        test(value: divideNumber(3), expected: "divide_by_3")
        test(value: divideNumber(5), expected: "divide_by_5")
        test(value: divideNumber(15), expected: "divibe_by_both_3_5")
    }

4. Test For Asynchronous code

    func testGetData(){
        let expectation = XCTestExpectation(description: "expectation async completed")
        Repository.loadData { data, messege in
            if let data = data?.first {
                XCTAssertEqual(data.title, "delectus aut autem")
                expectation.fulfill()
            }
        }
        wait(for: [expectation], timeout: 3.0)
    }
struct DataModel: Codable {
    let userID: Int
    let id: Int
    let title: String
    let completed: Bool

    enum CodingKeys: String, CodingKey {
        case userID = "userId"
        case id, title, completed
    }
}
class Repository {
    
    static func loadData(completion: @escaping(_ data: [DataModel]?, _ message: String?) -> Void){
        guard let url = URL(string: "https://jsonplaceholder.typicode.com/todos") else {
            print("Invalid API End Point")
            return
        }
        let request = URLRequest(url: url)
        
        URLSession.shared.dataTask(with: request) { data, response, error in
            if let data = data {
                if let response = try? JSONDecoder().decode([DataModel].self, from: data) {
                    completion(response, "successfully get data")
                }else{
                    completion(nil, "successfully went wrong")
                }
            }else{
                completion(nil, "successfully went wrong")
            }
        }.resume()
    }
    
}


Leap Year Problem:

“Write a function that, given an integer representing a year, tells whether the year is leap or not.”

Test Case:
1. A year evenly divisible by 4 is leap, for example, 2020.
2. A year evenly divisible by 100 is not leap, for example, 2100.
3. A year evenly divisible by 400 is leap, for example, 2000.
4. Any other year is not leap, for example, 2021.

When the behavior under test is not as trivial as a method taking one input and returning a Bool, it’s useful to structure the test by explicitly separating it into three stages:

Arrange: Prepare the inputs.
Act: Exercise the behavior under test.
Assert: Verify the output matches the expectation.

import XCTest

extension UnitTestExampleTests {
    
    func testEvenlyDivisibleBy4IsLeap() {
        let year = 2020 //Arrange
        let leap = isLeap(year) //Act
        XCTAssertTrue(leap) //Assert
    }
    
    func testEvenlyDivisibleBy100IsNotLeap() {
        XCTAssertFalse(isLeap(2100))
    }
    
    func testEvenlyDivisibleBy400IsLeap(){
        XCTAssertTrue(isLeap(2000))
    }
    
    func testNotEvenlyDivisibleBy4Or400IsNotLeap() {
        XCTAssertFalse(isLeap(2021))
    }
    
    func isLeap(_ year: Int) -> Bool {
        guard year % 400 != 0 else { return true }
        guard year % 100 != 0 else { return false }
        return year % 4 == 0
    }
    
}

Product Sold Problem:

Imagine you have a product value type defined like this:

struct Product {
    let category: String
    let price: Double
}

You need to write a function that, given an array of sold Products and a category value, returns the total amount sold for that category:

Test Case:
1. Test sum of the empty array is zero.
2. Test sum of one item is item price.
3. Test sum is sum of items prices for given category.

import XCTest

struct Product {
    let category: String
    let price: Double
}

extension UnitTestExampleTests {
    
    func testSumOfEmptyArrayIsZero(){
        
        let category = "books"
        let products = [Product]()
        
        let sum = sumOf(products, withCategory: category)
        
        XCTAssertEqual(sum, 0)
    }
    
    func testSumOfOneItemIsItemPrice(){
        let category = "books"
        let products = [ Product(category: category, price: 3)]
        
        let sum = sumOf(products, withCategory: category)
        
        XCTAssertEqual(sum, 3)
    }
    
    func testSumIsSumOfItemsPriceForGivenCategory(){
        let category = "books"
        let products = [
            Product(category: category, price: 3),
            Product(category: "movies", price: 2),
            Product(category: category, price: 1)
        ]
        
        let sum = sumOf(products, withCategory: category)
        
        XCTAssertEqual(sum, 4)
    }
    
    func sumOf(_ products: [Product], withCategory category: String) -> Double {
        return products
            .filter{ $0.category == category }
            .reduce(0.0) { $0 + $1.price }
    }
    
}

Key Takeaways:
1. Test List
2. Red, green, refactor
3. TDD lets you focus on making the code work first and then making it clean
4. Structure your test in three stages: Arrange, Act, Assert
5. Compiler errors are a source of feedback too
6. Fake it
7. Wishful Coding

Test List: Start listing all the tests you might need to verify the behavior of the code you need to implement.

Red, green, refactor. Tackling one test at a time, write the test, watch it fail because there is no implementation for that behavior, and then use the failure as a guide on what to do to make the test pass. Once the test passes, ask yourself whether the code can be improved.

TDD lets you focus on making the code work first and then making it clean. Tackling each task in isolation is easier than trying to do both at the same time. The tests give you the confidence to iterate on your implementation.

Structure your tests in three stages: Arrange, Act, Assert. Clearly and consistently separating the inputs, the behavior under test, and the expectation on its output will reduce the cognitive load of reading tests.

Compiler errors are a source of feedback too.

Fake It. If you are not sure how to implement something, start by hardcoding the value that will make the test pass. Once you have a green test, you can work on the real implementation with confidence.

Wishful Coding: If you are not sure how to define a function “or type, start by writing its usage in the test and use the compiler’s failure message as the starting point for the definition.