1
\$\begingroup\$

I have a protocol which has quite more methods, it looks like:

  protocol MessageService {

    //configuration
    var fetchOfflineMessagesOnLogin: Bool {get set}//default true
    var maxCharsInMessage: UInt {get set}//default 400

    //connection
    var connectionState: ConnectionState { get }
    func login(hostName: String, port: UInt, username: String, password: String)
    func logoutFromService()

    //contacts
    func sendContactRequest(to peerUId: String)
    func acceptContactRequest(from peerUId: String)
    func removeContact(peerUId: String)
  }

I am unit testing the controller class which takes this protocol and operates on it according to various states.
There are some functions in this controller class that only interacts with only one method out of many protocol methods, and I while testing those methods, I wrote following unit test:

func testSendContactRequest() {
    class MessageServiceMock: MessageService {
        func sendContactRequest(to peerUId: String) {
            //tests
        }
    }
    //unit test setup
    let controller = Controller(service: MessageServiceMock())
    //...
}

func testAcceptContactRequest() {
    class MessageServiceMock: MessageService {
        func acceptContactRequest(from peerUId: String) {
            //tests
        }
    }
    //unit test setup
    let controller = Controller(service: MessageServiceMock())
    //...
}

But, apparently, compiler throws error

MessageServiceMock does not conform to protocol

So I tried to make all the protocol methods optional by adding extension to it as (in test class file only):

fileprivate extension MessageService {

    var fetchOfflineMessagesOnLogin: Bool {
      get { return true }
      set {}
    }
    var maxCharsInMessage: UInt {
      get { return 400 }
      set {}
    }

    var connectionState: ConnectionState {
      return .connecting
    }
    func login(hostName: String, port: UInt, username: String, password: String) {}
    func logoutFromService() {}

    func sendContactRequest(to peerUId: String) {}
    func acceptContactRequest(from peerUId: String) {}
    func removeContact(peerUId: String) {}
}

Now error is gone and everything works fine.

I know other way to make protocol methods optional is using @objc optional keywords, but I can't use those as MessageService contains some Swift value types those are not convertible to ObjC objects.

So, is this solution is acceptable?
Are there any things that I can improve?

\$\endgroup\$
  • \$\begingroup\$ As per the help center rules, questions must contain code (or excerpts of code) from a real project. As a rule, xyz placeholders are not acceptable for Code Review. \$\endgroup\$ – 200_success Sep 7 '17 at 8:23
  • 1
    \$\begingroup\$ Replaced placeholder code by actual code \$\endgroup\$ – D4ttatraya Sep 7 '17 at 8:35
2
\$\begingroup\$

There are plenty of approaches that can fix your problem. But the main issue is how you build your classes. So IMHO you should focus on that.

Recommendation 1: You should build your classes with composition of protocols. Loggable, Runnable, Flyable etc. If you build your class with combination of many protocols, the problem would be solved. And since Swift enables default implementations for protocols you can write a shorter class that is build by protocols. In this case you can test these protocols and their implementations separately.

Recommendation 2: There is a code smell in this design. You say no need to test for most of the methods. But you still put them in the same interface. And inject a class that conforms to this interface. You should change the class structure or injection interface. As in first recommendation I offer you to separate those methods to several interfaces and inject each other separately.

let controller = Controller(service: ABCProtocolMock(), anotherService: XYZProtocolMock())

Recommendation 3: Code Complete 2nd Ed. offers classes should have no more than 7 methods. If a class has more methods they should be separated through many considerations including SOLID.

Recommendation 4: I don't support this solution, because its another hacky code smell. But as a lazy developer you can think about if only one method will be testable/replacable why do you inject a whole interface. You can just inject a small interface that provides that method only.

Recommendation 5: Using utility functions can be another hacky solution. Since you want to test just one method of the class and if this method's implementation can be abstracted from internals of your class. You can forward this method's implementation to a utility function and write unit tests for this utility function.

Recommendation 6: Another hacky solution is building your classes as NSObject childs, and using Method Swizzling for testing purposes

As I mentioned above I don't know what is your exact situation, but Recommendation 1-2-3 are the correct solutions for a solid rock architecture. Others are just hacks that can save your day, but through your situation you can prefer to choose one of them.

For this decision I can offer you to check; if this is a one-time issue you can use a hacky solution. But if this a common situation that effects your applications architecture you should definitely use Recommendation 1-2-3.

\$\endgroup\$
  • \$\begingroup\$ I am testing all the methods in controller class, but some of them only interacts with one protocol method. I've edited it. \$\endgroup\$ – D4ttatraya Sep 7 '17 at 8:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.