Swift Mocking Class

1.5k views Asked by At

As far as I know there is no possible solution for mocking and stubbing methods in swift like we were used in objc with OCMock, Mockito, etc.

I'm aware of technique described here. It is quite useful in some cases, but now I had a deadlock :)

I had a service layer where I had something like contracts(calling this method with this params will return that object as callback). This is one(greatly simplified) example:

class Bar
{
    func toData() -> NSData
    {
        return NSData()
    }
}

class Foo
{
    class func fromData(data: NSData) -> Foo
    {
        return Foo()
    }
}

class ServerManager
{
    let sharedInstance = ServerManager()

    class func send(request: NSData, response: (NSData)->())
    {
        //some networking code unrelated to the problem
        response(NSData())
    }
}

class MobileService1
{
    final class func Contract1(request: Bar, callback: (Foo) -> ())
    {
        ServerManager.send(request.toData()) { responseData in
            callback(Foo.fromData(responseData))
        }
    }
    //Contract2(...), Contract3(...), etc
}

Therefore somewhere in the code I had following scenario:

func someWhereInTheCode(someBool: Bool, someObject: Bar)
{
    if someBool
    {
        MobileService1.Contract1(someObject) { resultFoo in
            //self.Foo = resultFoo
        }
    }
    else
    {
        //MobileService1.Contract2(...)
    }
}

And the question now is how the heck could I test this? Is there better(for testing) alternative for code structure without touching contracts themselves?

3

There are 3 answers

1
hris.to On BEST ANSWER

Better late than never I found a solution. Just make dependency injection of the MobileService1(or better of it's interface) and then mock it easily:

//declaring interface
protocol MobileServiceContracts: class {
    static func Contract1(request: Bar, callback: (Foo) -> ())
}

//make implementation to conform to interface
class MobileService1 : MobileServiceContracts
{
    final class func Contract1(request: Bar, callback: (Foo) -> ())
    {
        ServerManager.send(request.toData()) { responseData in
            callback(Foo.fromData(responseData))
        }
    }
    //Contract2(...), Contract3(...), etc
}

//inject service
func someWhereInTheCode(someBool: Bool, someObject: Bar, serviceProvider: MobileServiceContracts.Type = MobileService1.self)
{
    if someBool
    {
        serviceProvider.Contract1(someObject) { resultFoo in
            //self.Foo = resultFoo
        }
    }
    else
    {
        //MobileService1.Contract2(...)
    }
}

Now you can easily change service in your tests:

class MockedMobileService1: MobileServiceContracts
{
    static func Contract1(request: Bar, callback: (Foo) -> ()) {
        //do whatever with the mock
    }
}

someWhereInTheCode(false, someObject: Bar(), serviceProvider: MockedMobileService1.self)

And the best part is with default values you can still call it the old way(not braking change):

someWhereInTheCode(false, someObject: Bar())
0
Jake On

There’s a much better framework called Mockingbird.

It’s super simple to setup and dynamically builds your mocks as the application builds to run the tests. Here’s an article that explains how some of it works

1
sundance On

Meanwhile, you can do mocking with Cuckoo, which is similar to Mockito.

Example Classes:

class ExampleObject {

    var number: Int = 0

    func evaluate(number: Int) -> Bool {
        return self.number == number
    }

}

class ExampleChecker {

    func check(object: ExampleObject) -> Bool {
        return object.evaluate(5)
    }

}

Example Test:

@testable import App
import Cuckoo
import XCTest

class ExampleCheckerTests: XCTestCase {

    func testCheck() {
        // 1. Arrange
        let object = MockExampleObject().spy(on: ExampleObject())
        stub(object) { object in
            when(object.evaluate(any())).thenDoNothing()
        }
        let checker = ExampleChecker()

        // 2. Action
        checker.check(object)

        // 3. Assert
        _ = verify(object).number.get
        verify(object).evaluate(any())
        verifyNoMoreInteractions(object)
    }

}