Generalization of nested Given/Perform/Verify
Hi folks!
Thanks for the awesome framework. I can’t imagine my unit testing experience without it and really appreciate your effort to develop and share it with a community :+1:
We extensively use SwiftyMocky in our team and currently we have around 200+ mocked protocols that gives us around 40k LoC of generated code. Somewhere around 20k LoC we started to experience compilation issues with a single Mocks.generated.swift and made few experiments with a different approaches. After few attempts we switched to the inline mocking and using it now. At the meantime we started to research for some solution to minimize the amount of the generated code and got the idea that could help to reduce a boilerplate code in generated mocks.
Each mock has four nested generated types - Given , Perform, Verify and MethodType as well as properies and helper methods to work with these types. The idea is to generalize Given, Perform and Verify and then extract helper methods and properies to the base generic mock class. This will allow to codegenerate only things related to the mocked protocol.
E.g let's assume we have the following protocol:
protocol AutoMockable {}
protocol Additive: AutoMockable {
func add(a: Int, b: Int) -> Int
}
The generic Given/Perform/Verify type could look like the following:
class Given<MethodType>: StubbedMethod {
var method: MethodType
init(method: MethodType, products: [StubProduct]) {
self.method = method
super.init(products)
}
}
This allows to move mock-specific static functions as an extension to the Given structure constrained to its MethodType
extension Given where MethodType == AdditiveMock.MethodType {
static func add(a: Parameter<Int>, b: Parameter<Int>, willReturn: Int...) -> MethodStub { /* .. */ }
}
And moreover to extract the default protocol implementation to the base mock class parametrized with MethodType generic
class BaseMock<MethodType>: Mock {
// ..
private var invocations: [MethodType] = []
private var methodReturnValues: [Given<MethodType>] = []
private var methodPerformValues: [Perform<MethodType>] = []
// ..
public func setupMock(file: StaticString = #file, line: UInt = #line) { /* .. */ }
func given(_ method: Given<MethodType>) { /* .. */ }
func perform(_ method: Perform<MethodType>) { /* .. */ }
func verify(_ method: Verify<MethodType>, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { /* .. */ }
// private helpers that work with generic
// ..
}
Now the mock implementation could only generate its MethodType and extensions to generic Given/Perform/Verify types.
class ManualAdditiveMock: BaseMock<ManualAdditiveMock.MethodType>, Additive {
func add(a: Int, b: Int) -> Int { /* .. */ }
enum MethodType: MethodTypeProtocol {
case m_add__a_ab_b(Parameter<Int>, Parameter<Int>)
static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Bool { /* .. */ }
func intValue() -> Int { /* .. */ }
}
}
extension Verify where T == ManualAdditiveMock.MethodType {
static func add(a: Parameter<Int>, b: Parameter<Int>) -> Verify { /* .. */ }
}
extension Perform where T == ManualAdditiveMock.MethodType {
static func add(a: Parameter<Int>, b: Parameter<Int>, perform: @escaping (Int, Int) -> Void) -> Perform { /* .. */ }
}
extension Given where T == ManualAdditiveMock.MethodType {
static func add(a: Parameter<Int>, b: Parameter<Int>, willReturn: Int...) -> MethodStub { /* .. */ }
static func add(a: Parameter<Int>, b: Parameter<Int>, willProduce: (Stubber<Int>) -> Void) -> MethodStub { /* .. */ }
}
I implemented simple mock using this approach manually to compare with the generated version and it alows to strip around 100 LoC. There is a Gist with a full source code.
Client-side usage of the mock through Given / Verify methods seems to be unchanged including the code completion.
I assume that the generalized code could either be added to the Runtime library or included as static part of the template.
Current idea is not a production-ready and still needs further evaluation for complex cases, such as PAT, generic functions, etc. so please let me know whether you think it's a viable idea. I would appreciate any feedback.
Thanks! :)
@fuzza this is perfect, thanks a lot! I will check it and try to have it in version 4.0 or 3.4, depending on complexity
Hi @amichnia! There is a quick follow-up on the topic.
Unfortunately, the initial approach does not work with PAT because implicit generics are inherited from a generated mock by nested structures, e.g:
class SomePATMock<A>: MockedProtocol {
enum MethodType {
.someCaseThatUsesGenericImplicitly(Parameter<A>)
}
}
extension Given where T == SomaPATMock<A>.MethodType { // This is not valid since A needs to be explicitly specified to satisfy the compiler
}
However, I was able to get this working by adding PAT for Given/Perform/Verify and replace inheritance from base mock with composition, so the generated mock decorates the implementation provided by the runtime. It is not so effective in terms of reducing a boilerplate amount but opens other possibilities to improve the mock internals.
If you are interested in such changes, would you mind if I submit a WIP PR that demonstrates the approach, so it could be discussed in more detail?
@fuzza thanks. The concerns stays still, as I understood generated file is just too big.
As a temporary workaround I would suggest:
- Inspect Mockfile
- Breakdown mock into separate files
I would suggest to do it feature/module based, so every important subdirectory in project can have it's own section in mockfile (with respectively different sources and output settings). That would generate several smaller files.