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?
-
\$\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– 200_success2017年09月07日 08:23:44 +00:00Commented Sep 7, 2017 at 8:23
-
1\$\begingroup\$ Replaced placeholder code by actual code \$\endgroup\$D4ttatraya– D4ttatraya2017年09月07日 08:35:18 +00:00Commented Sep 7, 2017 at 8:35
1 Answer 1
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.
-
\$\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– D4ttatraya2017年09月07日 08:15:57 +00:00Commented Sep 7, 2017 at 8:15