Framework for automatic mock generation. Adds a set of handy methods, simplifying testing. One of the best and most complete solutions, including generics support and much more.
MIT License
1.03k
stars
104
forks
source link
Feature request: Allow partial mocks for testable subclasses #340
Normally when building out new components or modules, we adopt the paradigm of a protocol as an interface. This lends itself really nicely to mocking with SwiftyMocky. As part of this, we prescribe to the idea that public functions in a class are only public if they are also in the protocol the class is conforming to. Other functions in the class that aren't in the protocol are still technically public, but as we'll use an instance of the class that has a type of the protocol, these methods won't be accessible. This means they can be interpreted as private, but can still be called during testing.
With this setup, it's really easy for us to test setupView() and buttonTapped() with SwiftyMocky by just doing
Verify(view, .updateData(.value([])))
The problem
If we update our HomeViewModel to perform so sorting or mutating in the setupView function before we call out to the view.updateData function, things get a little more complicated. Here's an updated example of the HomeViewModel with that change:
class HomeViewModel: HomeViewModelInterface {
private unowned let view: HomeViewInterface
init(view: HomeViewInterface) {
self.view = view
}
// MARK: - HomeViewModelInterface methods
func setupView() {
let data = []
let updatedData = injectAdditionalParts(to: data)
view.updateData(someData: updatedData)
}
func buttonTapped() {
view.showLoading(true)
}
// MARK: - Private methods
func injectAdditionalParts(to data: [Any]) -> [Any] {
// Add some new items to `data`
return data
}
}
Now when we go to test our setupView function, things have changed a bit. We can update our Verify call and edit the value we expect to get. Because injectAdditionalParts is also public, we can create a test for it and pass data into it and confirm it's returning what it should be. With those two updates we have things fairly well covered. But we only have them covered because injectAdditionalParts is returning a value. If this function had a method signature like func trackMetricForViewDidLoad() with no return value, how do we test that? We could expose it to the protocol and call it directly from the view. This isn't great though because we're now moving business logic decisions into our view. If we just want to check that a method has been called from another method, we have no easy way of doing this.
Proposal
When we setup our tests for the HomeViewModel it would look something like this:
class HomeViewModelTests: XCTestCase {
private var view: HomeViewInterfaceMock!
private var viewModel: HomeViewModel!
override func setUp() {
super.setUp()
view = HomeViewInterfaceMock()
viewModel = HomeViewModel(view: view)
}
}
My proposal is that we can mark the class HomeViewModel with another annotation like:
// sourcery: AutoMockablePartial
which would create a new HomeViewModelPartialMock, this could then be used in the following way:
class HomeViewModelTests: XCTestCase {
private var view: HomeViewInterfaceMock!
private var viewModel: HomeViewModelPartialMock!
override func setUp() {
super.setUp()
view = HomeViewInterfaceMock()
viewModel = HomeViewModelPartialMock(view: view, mockedMethods: [.injectAdditionalParts])
}
}
Here HomeViewModelPartialMock would subclass HomeViewModel and override only the injectAdditionalParts function, or any others passed in during init. We could therefore have a partial mock of the HomeViewModel.
How we use SwiftyMocky
Normally when building out new components or modules, we adopt the paradigm of a protocol as an interface. This lends itself really nicely to mocking with SwiftyMocky. As part of this, we prescribe to the idea that public functions in a class are only public if they are also in the protocol the class is conforming to. Other functions in the class that aren't in the protocol are still technically public, but as we'll use an instance of the class that has a type of the protocol, these methods won't be accessible. This means they can be interpreted as private, but can still be called during testing.
Example
With this setup, it's really easy for us to test
setupView()
andbuttonTapped()
with SwiftyMocky by just doingThe problem
If we update our
HomeViewModel
to perform so sorting or mutating in thesetupView
function before we call out to theview.updateData
function, things get a little more complicated. Here's an updated example of theHomeViewModel
with that change:Now when we go to test our
setupView
function, things have changed a bit. We can update ourVerify
call and edit the value we expect to get. BecauseinjectAdditionalParts
is also public, we can create a test for it and pass data into it and confirm it's returning what it should be. With those two updates we have things fairly well covered. But we only have them covered becauseinjectAdditionalParts
is returning a value. If this function had a method signature likefunc trackMetricForViewDidLoad()
with no return value, how do we test that? We could expose it to the protocol and call it directly from the view. This isn't great though because we're now moving business logic decisions into our view. If we just want to check that a method has been called from another method, we have no easy way of doing this.Proposal
When we setup our tests for the
HomeViewModel
it would look something like this:My proposal is that we can mark the class
HomeViewModel
with another annotation like:which would create a new
HomeViewModelPartialMock
, this could then be used in the following way:Here
HomeViewModelPartialMock
would subclassHomeViewModel
and override only theinjectAdditionalParts
function, or any others passed in duringinit
. We could therefore have a partial mock of theHomeViewModel
.