Skip to content

Commit e6abd33

Browse files
authored
Support user-defined configurations and profiles in Spring integration test code generation #2292 #2338 (#2351)
* Use binaryName instead of canonicalName for Spring configurations * Refactoring * Introduce SpringSettings * Replace PsiClassHelper.kt * Apply comment suggestions * Refactoring * Adapt annotation codegeneration * Apply comment suggestions
1 parent 670fdf7 commit e6abd33

File tree

34 files changed

+588
-506
lines changed

34 files changed

+588
-506
lines changed

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

Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1323,6 +1323,13 @@ enum class TypeReplacementMode {
13231323
NoImplementors,
13241324
}
13251325

1326+
interface CodeGenerationContext
1327+
1328+
interface SpringCodeGenerationContext : CodeGenerationContext {
1329+
val springTestType: SpringTestType
1330+
val springSettings: SpringSettings
1331+
}
1332+
13261333
/**
13271334
* A context to use when no specific data is required.
13281335
*
@@ -1332,7 +1339,7 @@ enum class TypeReplacementMode {
13321339
open class ApplicationContext(
13331340
val mockFrameworkInstalled: Boolean = true,
13341341
staticsMockingIsConfigured: Boolean = true,
1335-
) {
1342+
) : CodeGenerationContext {
13361343
var staticsMockingIsConfigured = staticsMockingIsConfigured
13371344
private set
13381345

@@ -1384,21 +1391,26 @@ open class ApplicationContext(
13841391
): Boolean = field.isFinal || !field.isPublic
13851392
}
13861393

1387-
sealed class TypeReplacementApproach {
1388-
/**
1389-
* Do not replace interfaces and abstract classes with concrete implementors.
1390-
* Use mocking instead of it.
1391-
*/
1392-
object DoNotReplace : TypeReplacementApproach()
1394+
sealed interface SpringConfiguration {
1395+
class JavaConfiguration(val classBinaryName: String) : SpringConfiguration
1396+
class XMLConfiguration(val absolutePath: String) : SpringConfiguration
1397+
}
13931398

1394-
/**
1395-
* Try to replace interfaces and abstract classes with concrete implementors
1396-
* obtained from bean definitions.
1397-
* If it is impossible, use mocking.
1398-
*
1399-
* Currently used in Spring applications only.
1400-
*/
1401-
class ReplaceIfPossible(val config: String) : TypeReplacementApproach()
1399+
sealed interface SpringSettings {
1400+
class AbsentSpringSettings : SpringSettings {
1401+
// Denotes no configuration and no profile setting
1402+
1403+
// NOTICE:
1404+
// `class` should not be replaced with `object`
1405+
// in order to avoid issues caused by Kryo deserialization
1406+
// that creates new instances breaking `when` expressions
1407+
// that check reference equality instead of type equality
1408+
}
1409+
1410+
class PresentSpringSettings(
1411+
val configuration: SpringConfiguration,
1412+
val profiles: Array<String>
1413+
) : SpringSettings
14021414
}
14031415

14041416
/**
@@ -1422,9 +1434,9 @@ class SpringApplicationContext(
14221434
staticsMockingIsConfigured: Boolean,
14231435
val beanDefinitions: List<BeanDefinitionData> = emptyList(),
14241436
private val shouldUseImplementors: Boolean,
1425-
val typeReplacementApproach: TypeReplacementApproach,
1426-
val testType: SpringTestsType
1427-
): ApplicationContext(mockInstalled, staticsMockingIsConfigured) {
1437+
override val springTestType: SpringTestType,
1438+
override val springSettings: SpringSettings,
1439+
): ApplicationContext(mockInstalled, staticsMockingIsConfigured), SpringCodeGenerationContext {
14281440

14291441
companion object {
14301442
private val logger = KotlinLogging.logger {}
@@ -1436,10 +1448,9 @@ class SpringApplicationContext(
14361448
private val springInjectedClasses: Set<ClassId>
14371449
get() {
14381450
if (!areInjectedClassesInitialized) {
1439-
// TODO: use more info from SpringBeanDefinitionData than beanTypeFqn offers here
1440-
for (beanFqn in beanDefinitions.map { it.beanTypeFqn }) {
1451+
for (beanTypeName in beanDefinitions.map { it.beanTypeName }) {
14411452
try {
1442-
val beanClass = utContext.classLoader.loadClass(beanFqn)
1453+
val beanClass = utContext.classLoader.loadClass(beanTypeName)
14431454
if (!beanClass.isAbstract && !beanClass.isInterface &&
14441455
!beanClass.isLocalClass && (!beanClass.isMemberClass || beanClass.isStatic)) {
14451456
springInjectedClassesStorage += beanClass.id
@@ -1449,7 +1460,7 @@ class SpringApplicationContext(
14491460
// it is possible to have problems with classes loading.
14501461
when (e) {
14511462
is ClassNotFoundException, is NoClassDefFoundError, is IllegalAccessError ->
1452-
logger.warn { "Failed to load bean class for $beanFqn (${e.message})" }
1463+
logger.warn { "Failed to load bean class for $beanTypeName (${e.message})" }
14531464

14541465
else -> throw e
14551466
}
@@ -1500,19 +1511,19 @@ class SpringApplicationContext(
15001511
): Boolean = field.fieldId in classUnderTest.allDeclaredFieldIds && field.declaringClass.id !in springInjectedClasses
15011512
}
15021513

1503-
enum class SpringTestsType(
1514+
enum class SpringTestType(
15041515
override val id: String,
15051516
override val displayName: String,
15061517
override val description: String,
15071518
// Integration tests generation requires spring test framework being installed
1508-
var frameworkInstalled: Boolean = false,
1519+
var testFrameworkInstalled: Boolean = false,
15091520
) : CodeGenerationSettingItem {
1510-
UNIT_TESTS(
1521+
UNIT_TEST(
15111522
"Unit tests",
15121523
"Unit tests",
15131524
"Generate unit tests mocking other classes"
15141525
),
1515-
INTEGRATION_TESTS(
1526+
INTEGRATION_TEST(
15161527
"Integration tests",
15171528
"Integration tests",
15181529
"Generate integration tests autowiring real instance"
@@ -1521,19 +1532,21 @@ enum class SpringTestsType(
15211532
override fun toString() = id
15221533

15231534
companion object : CodeGenerationSettingBox {
1524-
override val defaultItem = UNIT_TESTS
1525-
override val allItems: List<SpringTestsType> = values().toList()
1535+
override val defaultItem = UNIT_TEST
1536+
override val allItems: List<SpringTestType> = values().toList()
15261537
}
15271538
}
15281539

15291540
/**
15301541
* Describes information about beans obtained from Spring analysis process.
15311542
*
15321543
* Contains the name of the bean, its type (class or interface) and optional additional data.
1544+
*
1545+
* @param beanTypeName a name in a form obtained by [java.lang.Class.getName] method.
15331546
*/
15341547
data class BeanDefinitionData(
15351548
val beanName: String,
1536-
val beanTypeFqn: String,
1549+
val beanTypeName: String,
15371550
val additionalData: BeanAdditionalData?,
15381551
)
15391552

@@ -1542,11 +1555,13 @@ data class BeanDefinitionData(
15421555
*
15431556
* Sometimes the actual type of the bean can not be obtained from bean definition.
15441557
* Then we try to recover it by method and class defining bean (e.g. using Psi elements).
1558+
*
1559+
* @param configClassName a name in a form obtained by [java.lang.Class.getName] method.
15451560
*/
15461561
data class BeanAdditionalData(
15471562
val factoryMethodName: String,
15481563
val parameterTypes: List<String>,
1549-
val configClassFqn: String,
1564+
val configClassName: String,
15501565
)
15511566

15521567
val RefType.isAbstractType

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ object SpringModelUtils {
3131
val springBootTestContextBootstrapperClassId =
3232
ClassId("org.springframework.boot.test.context.SpringBootTestContextBootstrapper")
3333

34+
val activeProfilesClassId = ClassId("org.springframework.test.context.ActiveProfiles")
35+
val contextConfigurationClassId = ClassId("org.springframework.test.context.ContextConfiguration")
36+
3437

3538
// most likely only one persistent library is on the classpath, but we need to be able to work with either of them
3639
private val persistentLibraries = listOf("javax.persistence", "jakarta.persistence")

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ inline fun <T> withStaticsSubstitutionRequired(condition: Boolean, block: () ->
1414
} finally {
1515
UtSettings.substituteStaticsWithSymbolicVariable = standardSubstitutionSetting
1616
}
17-
}
17+
}

utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import org.utbot.framework.plugin.api.util.*
3939
import org.utbot.framework.util.convertToAssemble
4040
import org.utbot.framework.util.graph
4141
import org.utbot.framework.util.sootMethod
42+
import org.utbot.framework.plugin.api.SpringSettings.*
4243
import org.utbot.fuzzer.*
4344
import org.utbot.fuzzing.*
4445
import org.utbot.fuzzing.providers.FieldValueProvider
@@ -388,7 +389,7 @@ class UtBotSymbolicEngine(
388389
var testEmittedByFuzzer = 0
389390
val valueProviders = ValueProvider.of(defaultValueProviders(defaultIdGenerator))
390391
.letIf(applicationContext is SpringApplicationContext
391-
&& applicationContext.typeReplacementApproach is TypeReplacementApproach.ReplaceIfPossible
392+
&& applicationContext.springSettings is PresentSpringSettings
392393
) { provider ->
393394
val relevantRepositories = concreteExecutor.getRelevantSpringRepositories(methodUnderTest.classId)
394395
logger.info { "Detected relevant repositories for class ${methodUnderTest.classId}: $relevantRepositories" }
@@ -412,7 +413,7 @@ class UtBotSymbolicEngine(
412413
defaultIdGenerator,
413414
beanNameProvider = { classId ->
414415
(applicationContext as SpringApplicationContext).beanDefinitions
415-
.filter { it.beanTypeFqn == classId.name }
416+
.filter { it.beanTypeName == classId.name }
416417
.map { it.beanName }
417418
},
418419
relevantRepositories = relevantRepositories

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,8 @@ class SpringTestClassModel(
3030
val springSpecificInformation: SpringSpecificInformation,
3131
): TestClassModel(classUnderTest, methodTestSets, nestedClasses)
3232

33-
3433
class SpringSpecificInformation(
35-
val thisInstanceModels: TypedModelWrappers = mapOf(),
36-
val thisInstanceDependentMocks: TypedModelWrappers = mapOf(),
37-
val autowiredFromContextModels: TypedModelWrappers = mapOf(),
34+
val thisInstanceModels: TypedModelWrappers,
35+
val thisInstanceDependentMocks: TypedModelWrappers,
36+
val autowiredFromContextModels: TypedModelWrappers,
3837
)

utbot-framework/src/main/kotlin/org/utbot/framework/codegen/generator/SpringCodeGenerator.kt

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ import org.utbot.framework.plugin.api.ClassId
1717
import org.utbot.framework.plugin.api.CodegenLanguage
1818
import org.utbot.framework.plugin.api.ExecutableId
1919
import org.utbot.framework.plugin.api.MockFramework
20-
import org.utbot.framework.plugin.api.SpringTestsType
20+
import org.utbot.framework.plugin.api.SpringTestType
21+
import org.utbot.framework.plugin.api.SpringCodeGenerationContext
22+
import org.utbot.framework.plugin.api.SpringSettings.*
2123

2224
class SpringCodeGenerator(
23-
private val springTestsType: SpringTestsType = SpringTestsType.defaultItem,
2425
val classUnderTest: ClassId,
2526
val projectType: ProjectType,
27+
val codeGenerationContext: SpringCodeGenerationContext,
2628
paramNames: MutableMap<ExecutableId, List<String>> = mutableMapOf(),
2729
generateUtilClassFile: Boolean = false,
2830
testFramework: TestFramework = TestFramework.defaultItem,
@@ -59,9 +61,13 @@ class SpringCodeGenerator(
5961
val testClassModel = SpringTestClassModelBuilder(context).createTestClassModel(classUnderTest, testSets)
6062

6163
logger.info { "Code generation phase started at ${now()}" }
62-
val astConstructor = when (springTestsType) {
63-
SpringTestsType.UNIT_TESTS -> CgSpringUnitTestClassConstructor(context)
64-
SpringTestsType.INTEGRATION_TESTS -> CgSpringIntegrationTestClassConstructor(context)
64+
val astConstructor = when (codeGenerationContext.springTestType) {
65+
SpringTestType.UNIT_TEST -> CgSpringUnitTestClassConstructor(context)
66+
SpringTestType.INTEGRATION_TEST ->
67+
when (val settings = codeGenerationContext.springSettings) {
68+
is PresentSpringSettings -> CgSpringIntegrationTestClassConstructor(context, settings)
69+
is AbsentSpringSettings -> error("No Spring settings were provided for Spring integration test generation.")
70+
}
6571
}
6672
val testClassFile = astConstructor.construct(testClassModel)
6773
logger.info { "Code generation phase finished at ${now()}" }

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ abstract class CgAbstractSpringTestClassConstructor(context: CgContext):
2727

2828
protected val variableConstructor: CgSpringVariableConstructor =
2929
CgComponents.getVariableConstructorBy(context) as CgSpringVariableConstructor
30-
protected val statementConstructor: CgStatementConstructor = CgComponents.getStatementConstructorBy(context)
3130

3231
override fun constructTestClassBody(testClassModel: SpringTestClassModel): CgClassBody {
3332
return buildClassBody(currentTestClass) {
@@ -88,7 +87,7 @@ abstract class CgAbstractSpringTestClassConstructor(context: CgContext):
8887
annotationClassId: ClassId,
8988
groupedModelsByClassId: TypedModelWrappers,
9089
): List<CgFieldDeclaration> {
91-
val annotation = statementConstructor.addAnnotation(annotationClassId, Field)
90+
val annotation = addAnnotation(annotationClassId, Field)
9291

9392
val constructedDeclarations = mutableListOf<CgFieldDeclaration>()
9493
for ((classId, listOfUtModels) in groupedModelsByClassId) {
@@ -133,7 +132,7 @@ abstract class CgAbstractSpringTestClassConstructor(context: CgContext):
133132
}
134133

135134
protected fun constructBeforeMethod(statements: List<CgStatement>): CgFrameworkUtilMethod {
136-
val beforeAnnotation = statementConstructor.addAnnotation(context.testFramework.beforeMethodId, Method)
135+
val beforeAnnotation = addAnnotation(context.testFramework.beforeMethodId, Method)
137136
return CgFrameworkUtilMethod(
138137
name = "setUp",
139138
statements = statements,
@@ -143,7 +142,7 @@ abstract class CgAbstractSpringTestClassConstructor(context: CgContext):
143142
}
144143

145144
protected fun constructAfterMethod(statements: List<CgStatement>): CgFrameworkUtilMethod {
146-
val afterAnnotation = statementConstructor.addAnnotation(context.testFramework.afterMethodId, Method)
145+
val afterAnnotation = addAnnotation(context.testFramework.afterMethodId, Method)
147146
return CgFrameworkUtilMethod(
148147
name = "tearDown",
149148
statements = statements,

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

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,51 +7,96 @@ import org.utbot.framework.codegen.domain.TestNg
77
import org.utbot.framework.codegen.domain.context.CgContext
88
import org.utbot.framework.codegen.domain.models.*
99
import org.utbot.framework.codegen.domain.models.AnnotationTarget.*
10+
import org.utbot.framework.codegen.util.resolve
1011
import org.utbot.framework.plugin.api.ClassId
11-
import org.utbot.framework.plugin.api.CodegenLanguage
12+
import org.utbot.framework.plugin.api.SpringSettings.*
13+
import org.utbot.framework.plugin.api.SpringConfiguration.*
1214
import org.utbot.framework.plugin.api.util.SpringModelUtils
15+
import org.utbot.framework.plugin.api.util.SpringModelUtils.activeProfilesClassId
1316
import org.utbot.framework.plugin.api.util.SpringModelUtils.autoConfigureTestDbClassId
1417
import org.utbot.framework.plugin.api.util.SpringModelUtils.autowiredClassId
1518
import org.utbot.framework.plugin.api.util.SpringModelUtils.bootstrapWithClassId
19+
import org.utbot.framework.plugin.api.util.SpringModelUtils.contextConfigurationClassId
1620
import org.utbot.framework.plugin.api.util.SpringModelUtils.dirtiesContextClassId
1721
import org.utbot.framework.plugin.api.util.SpringModelUtils.dirtiesContextClassModeClassId
1822
import org.utbot.framework.plugin.api.util.SpringModelUtils.springBootTestContextBootstrapperClassId
1923
import org.utbot.framework.plugin.api.util.SpringModelUtils.springExtensionClassId
2024
import org.utbot.framework.plugin.api.util.SpringModelUtils.transactionalClassId
2125
import org.utbot.framework.plugin.api.util.utContext
2226

23-
class CgSpringIntegrationTestClassConstructor(context: CgContext) : CgAbstractSpringTestClassConstructor(context) {
27+
class CgSpringIntegrationTestClassConstructor(
28+
context: CgContext,
29+
private val springSettings: PresentSpringSettings
30+
) : CgAbstractSpringTestClassConstructor(context) {
2431
override fun constructTestClass(testClassModel: SpringTestClassModel): CgClass {
25-
collectSpringSpecificAnnotations()
32+
addNecessarySpringSpecificAnnotations()
2633
return super.constructTestClass(testClassModel)
2734
}
2835

2936
override fun constructClassFields(testClassModel: SpringTestClassModel): List<CgFieldDeclaration> {
30-
val autowiredFromContextModels = testClassModel.springSpecificInformation.autowiredFromContextModels
37+
val autowiredFromContextModels =
38+
testClassModel.springSpecificInformation.autowiredFromContextModels
3139
return constructFieldsWithAnnotation(autowiredClassId, autowiredFromContextModels)
3240
}
3341

34-
override fun constructAdditionalMethods() = CgMethodsCluster(header = null, content = emptyList())
42+
override fun constructAdditionalMethods() =
43+
CgMethodsCluster(header = null, content = emptyList())
3544

36-
private fun collectSpringSpecificAnnotations() {
45+
private fun addNecessarySpringSpecificAnnotations() {
3746
val springRunnerType = when (testFramework) {
3847
Junit4 -> SpringModelUtils.runWithClassId
3948
Junit5 -> SpringModelUtils.extendWithClassId
4049
TestNg -> error("Spring extension is not implemented in TestNg")
4150
else -> error("Trying to generate tests for Spring project with non-JVM framework")
4251
}
4352

44-
statementConstructor.addAnnotation(
53+
addAnnotation(
4554
classId = springRunnerType,
4655
argument = createGetClassExpression(springExtensionClassId, codegenLanguage),
4756
target = Class,
4857
)
49-
statementConstructor.addAnnotation(
58+
addAnnotation(
5059
classId = bootstrapWithClassId,
5160
argument = createGetClassExpression(springBootTestContextBootstrapperClassId, codegenLanguage),
5261
target = Class,
5362
)
54-
63+
addAnnotation(
64+
classId = activeProfilesClassId,
65+
namedArguments =
66+
listOf(
67+
CgNamedAnnotationArgument(
68+
name = "profiles",
69+
value =
70+
CgArrayAnnotationArgument(
71+
springSettings.profiles.map { profile ->
72+
profile.resolve()
73+
}
74+
)
75+
)
76+
),
77+
target = Class,
78+
)
79+
addAnnotation(
80+
classId = contextConfigurationClassId,
81+
namedArguments =
82+
listOf(
83+
CgNamedAnnotationArgument(
84+
name = "classes",
85+
value = CgArrayAnnotationArgument(
86+
listOf(
87+
createGetClassExpression(
88+
// TODO:
89+
// For now we support only JavaConfigurations in integration tests.
90+
// Adapt for XMLConfigurations when supported.
91+
ClassId((springSettings.configuration as JavaConfiguration).classBinaryName),
92+
codegenLanguage
93+
)
94+
)
95+
)
96+
)
97+
),
98+
target = Class,
99+
)
55100
addAnnotation(
56101
classId = dirtiesContextClassId,
57102
namedArguments = listOf(

0 commit comments

Comments
 (0)