As you start writing your first few unit tests, this is how you’re going to feel.
You’re testing a class, then you need to stub out some other class, and some more class. Eventually, you’ll feel like you’re basically stubbing everything. You don’t even know if you’re still testing the real class anymore.
I feel this pain when I first started unit testing many years ago. So I completely understand why you’re feeling this way. And I’m going to give you a magic pill for this pain to go away forever.
I came across the following question in Slack the other day:
Lets say that I have some
CommentsWebSocket
– and I inject it into in my view controller. Is there a reason to makeCommentsWebSocketStub
in order to inject it into thesut
when testing the view controller?
The question is in relation to the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
import Alamofire import Foundation import PusherSwift protocol CommentsWebSocketDelegate: class { func commentReceived(comment: Comment) } protocol CommentsWebSocketProtocol { func startWebSocket(delegate: CommentsWebSocketDelegate) } class CommentsWebSocket: CommentsWebSocketProtocol { init(commentDeserializer: Deserializer<Comment>, pusher: Pusher, lastCredentialsStore: LoginCredentialsStoring) { self.commentDeserializer = commentDeserializer self.pusher = pusher self.lastCredentialsStore = lastCredentialsStore } func startWebSocket(delegate: CommentsWebSocketDelegate) { guard let channelPin = lastCredentialsStore.lastCredentials?.pin else { fatalError("Error retrieving Pin from Stored Credentuals") } let channelName = "dashboard_channel_" pusher.connect() let debateChannel = pusher.subscribe("\(channelName)\(channelPin)") _ = debateChannel.bind(eventName: "comment_added") { (data: Any?) -> Void in guard let data = data as? [String: AnyObject] else { return } guard let commentBody = try? self.commentDeserializer.deserialize(json: data) else { return } delegate.commentReceived(comment: commentBody) } } |
Now, you don’t need to read and understand what the code does. You just need to know that the CommentsWebSocket
class conforms to the CommentsWebSocketProtocol
protocol. And the init()
accepts a CommentsWebSocketDelegate
using constructor dependency injection. You don’t need to read any of the code in the startWebSocket()
method to answer the question.
So, the answer is yes. When you’re testing the view controller, you stub out its external dependencies which is CommentsWebSocket
. When you’re testing CommentsWebSocket
, you also stub out its external dependencies which is CommentsWebSocketDelegate
.
However, you do not stub out the sut
because you wouldn’t be testing the real thing.
You want to stub because you do not want any change in the behavior of any external dependency to affect the results of your tests. For example, you don’t want a change in an API server to affect the test results of your iOS app, so you stub out the server. When you stub out the server, you only care if your sut
is making the appropriate API call to the server, but you don’t care what the server does with the request. Now, in the opposite direction, you provide canned responses from the server as inputs to test your sut
to see if the sut
is producing the correct outputs as a result.
So that’s the magic pill. And let me summarize it for you in the following so you don’t need a second visit to the doctor.
- Know what class you’re testing.
- Stub out that class’s dependencies.
- Do not stub out the class you’re testing.
Are you feeling better now?