Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Book] Pro iOS Testing: XCTest Framework for UI and Unit Testing #13

Open
ninokierulf opened this issue Nov 20, 2022 · 0 comments
Open

Comments

@ninokierulf
Copy link
Owner

ninokierulf commented Nov 20, 2022

By: Avi Tsadok 2020

Writing Tests

  • Arrange-Act-Assert (AAA) / Given-When-Then (GWT)
  • Custom Assertions
    - Duplicate Assertion Code
    - When your assertion code is too big
    - When you assertion doesn't speak the right language
  • Custom Assertions - Create
    - #file and #line - allows custom function to show fail feedback on correct test that failed
func checkPersonValues(  
    person: Person,  
    firstName : String,  
    lastName : String,   
    line : UInt = #line, // <--
    file : StaticString = #file // <-- 
) {  
    XCTAssertEqual(person.firstName, firstName, file: file, line:line)
    XCTAssertEqual(person.lastName, lastName, file: file, line:line)
}       
  • Asynchronous operations
    - Expect, Wait, Fulfill, and Assert
    - Define the Expectation. XCTestExpectation
    - Mark the Expectations as Fulfill
    - Pause the test method run until we have an answer
  • XCTestExpectation Pattern (see //1, //2, //3)
func testImageProcessing() {
    // arrange
    let image = UIImage(named: "cats")!
    let manager = CatsProcessingManager()

    // act
    var cuteCats = 0
    // creating an expectation to get number of cats.
    let expectation = self.expectation(description: "Counting number of cats") //1
    manager.findCuteCats(image: image) { (numberOfCuteCats) in
        cuteCats = numberOfCuteCats

        // we've got an answer. our expectation is fulfilled!
        expectation.fulfill() //2
    }

    // assert
    // let's wait 5 seconds before asserting...
    waitForExpectations(timeout: 5.0, handler: nil) //3
    XCTAssertEqual(cuteCats, 3)
}
  • Multiple fullfill, one expectation. expectedFulfillmentCount
let expectation = self.expectation(description: "executing closure code 3 times")
expectation.expectedFulfillmentCount = 3  // <--
  • Expect to not fullfill. isInverted
let expectation = self.expectation(description: "Code is not executed")
expectation.isInverted = true // <--
  • Expect array of expectations, ordered
wait(
    for: [loadFromFileExpectation, locateCuteCatsExpectation],
    timeout: 2.0
)
wait(
    for: [loadFromFileExpectation, locateCuteCatsExpectation],
    timeout: 2.0, 
    enforceOrder: true  // <-- 
)

Test Doubles

    - Dummy - Does nothing. Purpose: to fill the gap of instantiating objects
    - Fake - always returns the same value
    - Stub - control return value
    - Spy - does not return anything. To record calls that can be asserted  later
    - Mock - assertion happens in Mock via `verify()` via `Bool`
  • Complete vs Partial Mocking - Partial is when mocking is done via subclassing. Partial should be avoided if possible
  • Avoid test doubles if possible
    - Test doubles considered as a code smell.
  • Types of Coupling
    - Subclass
    - Shared Objects - diff objects mutating properties of shared obj
    - Dependencies - recommend to inject dependencies, delegate pattern, protocol-based dep, closure
    - Side Effects
  • Coupling Severity
    - Tightly coupled - cannot be replaced
    - Coupled - relies on particular type, able to change with obj of same type or subclass
    - Loosely coupled - not dependent on specific class but rather protocol. When you can use mocks/stubs
    - Decoupled - can communicate via closure or notification

Comparing

  • Equatable protocol. eg XCTAssertEqual(person1, person2)
extension Person : Equatable {
    static func ==(lhs: Person, rhs : Person)->Bool {
        return lhs.personID == rhs.personID
    }
}
  • Comparable protocol. eg XCTAssertGreaterThan(person1, person2)
extension Person : Comparable {
    static func < (lhs : Person, rhs: Person) -> Bool {
        return lhs.age < rhs.age
    }
}
  • Compare UIImages. eg XCTAssertEqual(image1, image2)
func ==(lhs: UIImage, rhs: UIImage) -> Bool {
    if let lhsData = lhs.pngData(), let rhsData = rhs.pngData() {
        return lhsData == rhsData
    }
    return false
}
  • Compare Arrays
    - Items in the array, if not primitive, should be Comparable / Equatable
    - do you need to test order? or not?
    - does duplicate items matter in your test

Parameterized Unit Tests

  • Create Abstract Method for Testing. eg runTest
func runTest(
    withData events : [Event], 
    expectedLayout : LayoutStructure, 
    file : StaticString = #file, line : UInt = #line
) {
    // act
    let actualStructure = CalendarLayoutGenerator().
    generateLayout(events: events)
    // assert
    XCTAssertEqual(actualStructure, expectedLayout, file :
    file, line : line)
}

func testGenerateLayout_abstractMethod1() {
    var events = [Event]()
    events.append(Event(...)) // different data set
    ...

    let expectedLayout = LayoutStructure()

    runTest(withData: events, expectedLayout: LayoutStructure())  // <--
}

func testGenerateLayout_abstractMethod2() {
    var events = [Event]()
    events.append(Event(...))
    ...

    let expectedLayout = LayoutStructure()

    runTest(withData: events, expectedLayout: LayoutStructure()) //  <--
}
  • Loading test cases from a file
    - effortless to add more tests cases
    - easy to validate
    - anybody can write
    - excellent for cross-platform testing
    - Bundle(for: type(of: self)).path(forResource: "tests", ofType: "json")
{
   "test":[
      {
         "name":"test1",
         "events":[
            {
               "startDate":"04/27/2020 10:00",
               "endDate":"04/27/2020 11:00"
            },
            {
               "startDate":"04/27/2020 10:30",
               "endDate":"04/27/2020 11:20"
            },
            {
               "startDate":"04/28/2020 12:30",
               "endDate":"04/28/2020 12:30"
            }
         ],
         "expectedStructure":"--==--"
      },
      {
         "name":"test2",
         "events":[
            {
               "startDate":"04/27/2020 10:30",
               "endDate":"04/27/2020 11:30"
            },
            {
               "startDate":"04/27/2020 10:45",
               "endDate":"04/27/2020 11:50"
            },
            {
               "startDate":"04/28/2020 12:20",
               "endDate":"04/28/2020 15:30"
            }
         ],
         "expectedStructure":"--==--"
      }
   ]
}
  • Invoke tests dynamically
    1. Override defaultTestSuite() variable - return own own XCTestSuite
    2. `let testSuite = XCTestSuite(name: NSStringFromClass(self))
    3. Create and add new test cases on the fly
class FullNamesGeneratorTests: XCTestCase {
    //1
    var names = [String]()
    var expectedFullName = ""

    //2
    override class var defaultTestSuite: XCTestSuite {
        get {
            let testSuite = XCTestSuite(name: NSStringFromClass(self))

            addNewTest(withNames: ["Avi", "Tsadok"],
            expectedResult: "Avi Tsadok", testSuite: testSuite)
            addNewTest(withNames: ["Bill", "Gates"],
            expectedResult: "Bill Gates", testSuite: testSuite)
            addNewTest(withNames: ["Steve", "Jobs"],
            expectedResult: "Steve Ballmer", testSuite: testSuite)

            return testSuite
        }
    }

    //3
    class func addNewTest(
        withNames names : [String],
        expectedResult : String, 
        testSuite : XCTestSuite
    ) {
        for invocation in self.testInvocations {

            let newTestCase = FullNamesGeneratorTests(invocati
            on: invocation)
            newTestCase.names = names
            newTestCase.expectedFullName = expectedResult

            testSuite.addTest(newTestCase)
        }
    }

    //4
    func testFullNameGenerator() {
        var fullName = ""
        for name in names {
            fullName += name 
            if name != names.last! {
                fullName += " "
            }
        }

        XCTAssertEqual(fullName, expectedFullName)
    }
}
1) `names`, `expectedFullName` - input  and expected output from test method
2) Creation of new  test suite with several test cases
3) `addNewTest` - for every `invocation` creates new test case into test suite
4) actual test method

<reading> expect more </reading>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant