Feature request: Allow partial mocks for testable subclasses
mapierce opened this issue · 0 comments
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
// sourcery: AutoMockable
protocol HomeViewInterface {
func showLoading(show: Bool)
func updateData(someData: [Any])
}
protocol HomeViewModelInterface {
func setupView()
func buttonTapped()
}
class HomeViewController: UIViewController, HomeViewInterface {
var viewModel: HomeViewModelInterface!
override func viewDidLoad() {
super.viewDidLoad()
viewModel.setupView()
}
// MARK: - HomeViewInterface methods
func showLoading(show: Bool) {
// Hide / show loading...
}
func updateData(someData: [Any]) {
// Update data...
}
}
class HomeViewModel: HomeViewModelInterface {
private unowned let view: HomeViewInterface
init(view: HomeViewInterface) {
self.view = view
}
// MARK: - HomeViewModelInterface methods
func setupView() {
view.updateData(someData: [])
}
func buttonTapped() {
view.showLoading(true)
}
}
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
.