Skip to the content.

Last commit build

Mimic

What is mimic?

Mimic is a testing framework designed specifically for the Swift programming language. It offers two key features that simplify the testing process.

Since Swift's reflection is limited and rather we call it to introspection, creating runtime mocking frameworks are hardly possible. Most of the solutions are working with build time mock generations where creating mocks are marked with protocols or comments. This solution was created because we felt these solutions a bit unconvinient or not clean enough from a software development point of view. Of course perfect solution doesn’t exist, and this is also just a leaky abstraction , the trade of here is that we have to create fake classes manually but this package helps us to make this painful method as convinient as possible. Moreover to be able to replace the behaviour of a class it forces the developer to organize the code in a clean way.

Faking

By using fake objects, you can simulate various scenarios and ensure that your code behaves as expected in different situations by isolating the behaviors of specific components during testing. Mimic provides a simple and intuitive API that allows you to specify the expected behavior and responses of the fake object. This way, you can precisely control the interactions between your tests and the fake object, making testing more reliable and reproducible.

Creating fake objects

Lets say we have a protocol which defines a function.

protocol SomeProtocol {
    
    func someFunction(with parameter: String) -> Int
    
}

And we have a class which conforms to this protocol and adds some implementation for that.

class SomeClass: SomeProtocol {
    
    func someFunction(with parameter: String) -> Int {
        return 42
    }
    
}

So now comes the tricky part, to create a fake object that could be used from tests for SomeClass class, we should do this:

final class FakeSomeClass: SomeProtocol, Mimic {
    
    let fnSomeFunction = Fn<Int>()
    
    func someFunction(with parameter: String) -> Int {
        return try! fnSomeFunction.invoke(params: parameter)
    }
    
}

Key steps:

That’s it. So as it is visible from the example above, it needs some code typing, but not so terrible, no need to add internal logic, or enything else, just a pure declaration, and teachings and verifications will work.

Using fake objects

Checking properties

If we have a fake class that has properties, we can easily check their values by creating an expected dictionary where the key is the name of the property and the values are what we expecting, than make Props object from both and check their equality.

    let expected = [
        "boolValue": true,
        "wholeValue": 42,
        "floatingValue": 3.1415926535897,
        "stringValue": "Oh, yeah. Oooh, ahhh, that's how it always starts. Then later there's running and um, screaming.",
        "array": [true, false, true],
        "dict": ["key": "value"],
        "optionalNil": nil
    ].toProps()
    
    let result = testStruct.props()
    
    XCTAssertEqual(result, expected)

when used for teaching / stubbing

replaceFunction

Sometimes we need more control on the function.

    let expected = 496
    
    var parameter: String!
    var times = 0

    fakeSomeClass.when(\.fnSomeFunction).replaceFunction { invocationCount, params in
        times = invocationCount
        parameter = params[0]
        return expected
    }
    
    let result = testStruct.someFunction(with: "parameterValue")
    
    XCTAssertEqual(result, expected)
    XCTAssertEqual(parameter, "parameterValue")
    XCTAssertEqual(times, 1)

Key steps:

thenReturn

Sometimes we just want to stub it and return with a simple value.

    let expected = 496

    fakeSomeClass.when(\.fnSomeFunction).thenReturn(expected)
    
    let result = testStruct.someFunction(with: "parameterValue")
    
    XCTAssertEqual(result, expected)

Key steps:

thenReturns

Sometimes we just want to stub it and return with a simple value.

    let expected1 = 6
    let expected2 = 28
    let expected3 = 496
    let expected5 = 8128

    fakeSomeClass.when(\.fnSomeFunction).thenReturns(expected1, expected2 , expected3, nil, expected4)
    
    let result1 = testStruct.someFunction(with: "parameterValue")
    let result2 = testStruct.someFunction(with: "parameterValue")
    let result3 = testStruct.someFunction(with: "parameterValue")
    let result4 = testStruct.someFunction(with: "parameterValue")
    let result5 = testStruct.someFunction(with: "parameterValue")
    
    XCTAssertEqual(result1, expected1)
    XCTAssertEqual(result2, expected2)
    XCTAssertEqual(result3, expected3)
    XCTAssertNil(result4)
    XCTAssertEqual(result5, expected5)

Key steps:

matchers

Just like in mocking frameworks, mimic has matchers as well, it could be use for teaching and verification.

    let expectedArgument = "testExpectedArgument"

    fakeSomeClass.when(\.fnSomeFunction).calledWith(Arg.eq(expectedArgument)).thenReturn(42)
    
    let result = testStruct.someFunction(with: expectedArgument)
    
    XCTAssertEqual(result, 42)

Key steps:

thenThrow

It is just simply throw the given Error when the function being called.

    fakeSomeClass.when(\.fnSomeFunction).thenThrow(error: TestError.magicWord)
    
    let result = testStruct.someFunction(with: "value")

verify used for verification after the function usage

onThread(<Thread>)

This could be used to check on which thread the function was called.

    try fakeSomeClass.verify(\.fnSomeFunction).on(thread: Thread.current)

times(<times value>)

This could be used to check how many times the function was called.

    try fakeSomeClass.verify(\.fnSomeFunction).times(times: .atLeast(2))

Possible values:

Instance Generation for Codable Data Classes

When working with data classes, it’s often necessary to create instances of them for testing purposes. Mimic simplifies this process by automatically generating instances with random values. This feature becomes especially handy when dealing with complex data models or when you need to generate a large number of instances for testing different scenarios. With this feature you can save time and effort that you could spend instead for the actual testing.

@Generate

To use this feature you just simply add @Generate attribute before your variable.

    @Generate
    var someStruct: SomeStruct

That’s it, after this code you could simply use the someStruct variable.

Limitations:

The cause

This team is the maker of the EmarsysSDK’s when we started to develop the swift based version of it, we shortly arrived that point when we needed a proper mocking solution. We tried out few, but non of them was fit perfectly to our needs mainly because of those points what are highlighted above. This package and it’s features are actively used in our SDK. Our team strongly beliefs that the good things, findings, solutions, knowledge should be shared that’s why we made this repo publicly available to anyone. We hope you will consider it useful and will help your work.

No promises

Disclaimer

As we mentioned before, at first place this package was created for internal use only. The main goal was to cover all of our use cases and fulfill our requirements what doesn’t mean that this is good for everyone. If you find lack of features or issues we do not say that those will be implemented or solved in the near future since this product is not actively developed. We do our best, but no promises.

The logo was created by Bing Image Creator