Skip to content

Commit 0e79564

Browse files
authored
Support autowiring collections in Spring Unit tests (#2473)
* Refactoring * Add SpyFrameworkManager * Add correct fieldWithAnnotationIsRequired method * Small refactoring * Add new collector of dependent models * Refactoring
1 parent 68a5dbb commit 0e79564

File tree

12 files changed

+198
-113
lines changed

12 files changed

+198
-113
lines changed

utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/Api.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,18 @@ fun UtModel.hasDefaultValue() =
398398
*/
399399
fun UtModel.isMockModel() = this is UtCompositeModel && isMock
400400

401+
/**
402+
* Checks that this [UtModel] must be constructed with @Spy annotation in generated tests.
403+
* Used only for construct variables with @Spy annotation.
404+
*/
405+
fun UtModel.canBeSpied(): Boolean {
406+
val javaClass = this.classId.jClass
407+
408+
return this is UtAssembleModel &&
409+
(Collection::class.java.isAssignableFrom(javaClass)
410+
|| Map::class.java.isAssignableFrom(javaClass))
411+
}
412+
401413
/**
402414
* Get model id (symbolic null value for UtNullModel)
403415
* or null if model has no id (e.g., a primitive model) or the id is null.

utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/Domain.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -757,12 +757,9 @@ object SpringBoot : DependencyInjectionFramework(
757757
*
758758
* Used as a key in [valueByUtModelWrapper].
759759
* Was introduced primarily for shared among all test methods global variables.
760-
*
761-
* `modelTagName` is used to distinguish between variables with annotation @Mock that have the same model
762760
*/
763761
data class UtModelWrapper(
764762
val testSetId: Int,
765763
val executionId: Int,
766764
val model: UtModel,
767-
val modelTagName: String?
768765
)

utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/builtin/MockitoBuiltins.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ internal val mockClassId = BuiltinClassId(
2525
simpleName = "Mock",
2626
)
2727

28+
internal val spyClassId = BuiltinClassId(
29+
canonicalName = "org.mockito.Spy",
30+
simpleName = "Spy"
31+
)
32+
2833
internal val injectMocksClassId = BuiltinClassId(
2934
canonicalName = "org.mockito.InjectMocks",
3035
simpleName = "InjectMocks",

utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/builtin/UtilMethodBuiltins.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,26 @@ val closeMethodId = MethodId(
318318
parameters = emptyList(),
319319
)
320320

321+
private val clearCollectionMethodId = MethodId(
322+
classId = Collection::class.java.id,
323+
name = "clear",
324+
returnType = voidClassId,
325+
parameters = emptyList()
326+
)
327+
328+
private val clearMapMethodId = MethodId(
329+
classId = Map::class.java.id,
330+
name = "clear",
331+
returnType = voidClassId,
332+
parameters = emptyList()
333+
)
334+
335+
fun clearMethodId(javaClass: Class<*>): MethodId = when {
336+
Collection::class.java.isAssignableFrom(javaClass) -> clearCollectionMethodId
337+
Map::class.java.isAssignableFrom(javaClass) -> clearMapMethodId
338+
else -> error("Clear method is not implemented for $javaClass")
339+
}
340+
321341
val mocksAutoCloseable: Set<ClassId> = setOf(
322342
MockitoStaticMocking.mockedStaticClassId,
323343
MockitoStaticMocking.mockedConstructionClassId

utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/context/CgContext.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -457,12 +457,11 @@ interface CgContextOwner {
457457
val getLambdaMethod: MethodId
458458
get() = utilMethodProvider.getLambdaMethodMethodId
459459

460-
fun UtModel.wrap(modelTagName: String? = null): UtModelWrapper =
460+
fun UtModel.wrap(): UtModelWrapper =
461461
UtModelWrapper(
462462
testSetId = currentTestSetId,
463463
executionId = currentExecutionId,
464464
model = this,
465-
modelTagName = modelTagName
466465
)
467466
}
468467

utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/models/TestClassModel.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,6 @@ class SpringTestClassModel(
3333
class SpringSpecificInformation(
3434
val thisInstanceModels: TypedModelWrappers,
3535
val thisInstanceDependentMocks: TypedModelWrappers,
36+
val thisInstanceDependentSpies: TypedModelWrappers,
3637
val autowiredFromContextModels: TypedModelWrappers,
3738
)

utbot-framework/src/main/kotlin/org/utbot/framework/codegen/domain/models/builders/SpringTestClassModelBuilder.kt

Lines changed: 35 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,19 @@ import org.utbot.framework.plugin.api.UtAssembleModel
1212
import org.utbot.framework.plugin.api.UtClassRefModel
1313
import org.utbot.framework.plugin.api.UtCompositeModel
1414
import org.utbot.framework.plugin.api.UtCustomModel
15-
import org.utbot.framework.plugin.api.UtDirectSetFieldModel
1615
import org.utbot.framework.plugin.api.UtEnumConstantModel
1716
import org.utbot.framework.plugin.api.UtLambdaModel
1817
import org.utbot.framework.plugin.api.UtModel
1918
import org.utbot.framework.plugin.api.UtNullModel
2019
import org.utbot.framework.plugin.api.UtPrimitiveModel
21-
import org.utbot.framework.plugin.api.UtStatementCallModel
2220
import org.utbot.framework.plugin.api.UtVoidModel
2321
import org.utbot.framework.plugin.api.isMockModel
2422
import org.utbot.framework.plugin.api.util.SpringModelUtils.isAutowiredFromContext
23+
import org.utbot.framework.plugin.api.canBeSpied
2524

2625
typealias TypedModelWrappers = Map<ClassId, Set<UtModelWrapper>>
2726

28-
class SpringTestClassModelBuilder(val context: CgContext):
27+
class SpringTestClassModelBuilder(val context: CgContext) :
2928
TestClassModelBuilder(),
3029
CgContextOwner by context {
3130

@@ -54,13 +53,13 @@ class SpringTestClassModelBuilder(val context: CgContext):
5453
.filterNotNull()
5554
.forEach { model ->
5655
thisInstanceModels += model.wrap()
57-
thisInstancesDependentModels += collectByModel(model)
56+
thisInstancesDependentModels += collectDependentModels(model)
5857

5958
}
6059

6160
(execution.stateBefore.parameters + execution.stateBefore.thisInstance)
6261
.filterNotNull()
63-
.forEach { model -> stateBeforeDependentModels += collectByModel(model) }
62+
.forEach { model -> stateBeforeDependentModels += collectDependentModels(model) }
6463
}
6564
}
6665
}
@@ -72,78 +71,66 @@ class SpringTestClassModelBuilder(val context: CgContext):
7271
cgModel.model.isMockModel() && cgModel !in thisInstanceModels
7372
}
7473

74+
val dependentSpyModels =
75+
thisInstancesDependentModels
76+
.filterTo(mutableSetOf()) { cgModel ->
77+
cgModel.model.canBeSpied() && cgModel !in thisInstanceModels && cgModel !in dependentMockModels
78+
}
79+
7580
val autowiredFromContextModels =
7681
stateBeforeDependentModels.filterTo(HashSet()) { it.model.isAutowiredFromContext() }
7782

7883
return SpringSpecificInformation(
7984
thisInstanceModels.groupByClassId(),
8085
dependentMockModels.groupByClassId(),
86+
dependentSpyModels.groupByClassId(),
8187
autowiredFromContextModels.groupByClassId(),
8288
)
8389
}
8490

85-
private fun collectByModel(model: UtModel): Set<UtModelWrapper> {
91+
private fun collectDependentModels(model: UtModel): Set<UtModelWrapper> {
8692
val dependentModels = mutableSetOf<UtModelWrapper>()
8793

88-
collectRecursively(model.wrap(), dependentModels)
89-
90-
return dependentModels
91-
}
92-
93-
private fun Set<UtModelWrapper>.groupByClassId(): TypedModelWrappers {
94-
val classModels = mutableMapOf<ClassId, Set<UtModelWrapper>>()
95-
96-
for (modelGroup in this.groupBy { it.model.classId }) {
97-
classModels[modelGroup.key] = modelGroup.value.toSet()
98-
}
99-
100-
return classModels
101-
}
102-
103-
private fun collectRecursively(currentModelWrapper: UtModelWrapper, allModels: MutableSet<UtModelWrapper>) {
104-
if (!allModels.add(currentModelWrapper)) {
105-
return
106-
}
107-
108-
when (val currentModel = currentModelWrapper.model) {
94+
when (model) {
10995
is UtNullModel,
11096
is UtPrimitiveModel,
11197
is UtClassRefModel,
11298
is UtVoidModel,
11399
is UtEnumConstantModel,
114-
is UtCustomModel-> {}
100+
is UtCustomModel -> {}
115101
is UtLambdaModel -> {
116-
currentModel.capturedValues.forEach { collectRecursively(it.wrap(), allModels) }
102+
model.capturedValues.forEach { dependentModels.add(it.wrap()) }
117103
}
118104
is UtArrayModel -> {
119-
currentModel.stores.values.forEach { collectRecursively(it.wrap(), allModels) }
120-
if (currentModel.stores.count() < currentModel.length) {
121-
collectRecursively(currentModel.constModel.wrap(), allModels)
105+
model.stores.values.forEach { dependentModels.add(it.wrap()) }
106+
if (model.stores.count() < model.length) {
107+
dependentModels.add(model.constModel.wrap())
122108
}
123109
}
124110
is UtCompositeModel -> {
125111
// Here we traverse fields only.
126112
// Traversing mocks as well will result in wrong models playing
127113
// a role of class fields with @Mock annotation.
128-
currentModel.fields.forEach { (fieldId, model) ->
129-
// We use `modelTagName` in order to distinguish mock models
130-
val modeTagName = if(model.isMockModel()) fieldId.name else null
131-
collectRecursively(model.wrap(modeTagName), allModels)
132-
}
114+
model.fields.forEach { (_, model) -> dependentModels.add(model.wrap()) }
133115
}
134116
is UtAssembleModel -> {
135-
currentModel.instantiationCall.instance?.let { collectRecursively(it.wrap(), allModels) }
136-
currentModel.instantiationCall.params.forEach { collectRecursively(it.wrap(), allModels) }
137-
138-
currentModel.modificationsChain.forEach { stmt ->
139-
stmt.instance?.let { collectRecursively(it.wrap(), allModels) }
140-
when (stmt) {
141-
is UtStatementCallModel -> stmt.params.forEach { collectRecursively(it.wrap(), allModels) }
142-
is UtDirectSetFieldModel -> collectRecursively(stmt.fieldModel.wrap(), allModels)
143-
}
144-
}
117+
model.instantiationCall.instance?.let { dependentModels.add(it.wrap()) }
118+
model.instantiationCall.params.forEach { dependentModels.add(it.wrap()) }
145119
}
146-
//Python, JavaScript, Go models are not required in Spring
147120
}
121+
122+
return dependentModels
123+
}
124+
125+
126+
private fun Set<UtModelWrapper>.groupByClassId(): TypedModelWrappers {
127+
val classModels = mutableMapOf<ClassId, Set<UtModelWrapper>>()
128+
129+
for (modelGroup in this.groupBy { it.model.classId }) {
130+
classModels[modelGroup.key] = modelGroup.value.toSet()
131+
}
132+
133+
return classModels
148134
}
135+
149136
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.utbot.framework.codegen.services.framework
2+
3+
import org.utbot.framework.codegen.domain.context.CgContext
4+
import org.utbot.framework.plugin.api.UtAssembleModel
5+
6+
class SpyFrameworkManager(context: CgContext) : CgVariableConstructorComponent(context) {
7+
8+
fun spyForVariable(model: UtAssembleModel){
9+
variableConstructor.constructAssembleForVariable(model)
10+
}
11+
12+
}

utbot-framework/src/main/kotlin/org/utbot/framework/codegen/tree/CgAbstractSpringTestClassConstructor.kt

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package org.utbot.framework.codegen.tree
22

3-
import org.utbot.framework.codegen.domain.UtModelWrapper
43
import org.utbot.framework.codegen.domain.builtin.TestClassUtilMethodProvider
54
import org.utbot.framework.codegen.domain.context.CgContext
65
import org.utbot.framework.codegen.domain.models.AnnotationTarget.*
@@ -19,7 +18,6 @@ import org.utbot.framework.codegen.domain.models.SpringTestClassModel
1918
import org.utbot.framework.codegen.domain.models.builders.TypedModelWrappers
2019
import org.utbot.framework.plugin.api.UtExecution
2120
import org.utbot.framework.plugin.api.UtSpringContextModel
22-
import org.utbot.framework.plugin.api.util.SpringModelUtils.getBeanNameOrNull
2321
import org.utbot.framework.plugin.api.util.id
2422
import java.lang.Exception
2523

@@ -102,18 +100,18 @@ abstract class CgAbstractSpringTestClassConstructor(context: CgContext) :
102100
val constructedDeclarations = mutableListOf<CgFieldDeclaration>()
103101
for ((classId, modelWrappers) in groupedModelsByClassId) {
104102

105-
val fieldWithAnnotationIsRequired = fieldManager.fieldWithAnnotationIsRequired(modelWrappers)
103+
val modelWrapper = modelWrappers.firstOrNull() ?: continue
104+
val model = modelWrapper.model
105+
106+
val fieldWithAnnotationIsRequired = fieldManager.fieldWithAnnotationIsRequired(model.classId)
106107
if (!fieldWithAnnotationIsRequired) {
107108
continue
108109
}
109110

110-
val modelWrapper = modelWrappers.firstOrNull() ?: continue
111-
val model = modelWrapper.model
112-
113-
val baseVarName = model.getBeanNameOrNull()
111+
val baseVarName = fieldManager.constructBaseVarName(model)
114112

115113
val createdVariable = variableConstructor.getOrCreateVariable(model, baseVarName) as? CgVariable
116-
?: error("`UtCompositeModel` model was expected, but $model was found")
114+
?: error("`CgVariable` cannot be constructed from a $model model")
117115

118116
val declaration = CgDeclaration(classId, variableName = createdVariable.name, initializer = null)
119117

@@ -126,17 +124,10 @@ abstract class CgAbstractSpringTestClassConstructor(context: CgContext) :
126124
modelWrappers
127125
.forEach { modelWrapper ->
128126

129-
val modelWrapperWithNullTagName = UtModelWrapper(
130-
testSetId = modelWrapper.testSetId,
131-
executionId = modelWrapper.executionId,
132-
model = modelWrapper.model,
133-
modelTagName = null,
134-
)
135-
136-
valueByUtModelWrapper[modelWrapperWithNullTagName] = createdVariable
127+
valueByUtModelWrapper[modelWrapper] = createdVariable
137128

138129
variableConstructor.annotatedModelGroups
139-
.getOrPut(annotationClassId) { mutableSetOf() } += modelWrapperWithNullTagName
130+
.getOrPut(annotationClassId) { mutableSetOf() } += modelWrapper
140131
}
141132
}
142133

0 commit comments

Comments
 (0)