PY-38775: Generate tests even for empty classes.

When user wants to generate test for class ``SpamEggs`` that has no methods:
* For pytest generate "test_spam_eggs" function
* for UniTest "class TestSpamEggs"

``PyTestCreationModel`` also rewritten a little to make it slightly more readable.

GitOrigin-RevId: cc5d774ad684844f5fd9c86634dbcd0d2343437f
This commit is contained in:
Ilya.Kazakevich
2019-11-07 02:40:58 +00:00
committed by intellij-monorepo-bot
parent d05a117091
commit b4e6e537b6
3 changed files with 97 additions and 60 deletions
@@ -22,37 +22,56 @@ class PyTestCreationModel(var fileName: String,
var methods: List<String>) {
init {
assert(methods.isNotEmpty()) { "Provide at least one method" }
assert(className.isNotEmpty() || methods.isNotEmpty()) { "Either class or at least one method must be provided" }
}
companion object {
private val String.asFunName
get():String {
return replace(Regex("([a-z])([A-Z])"), "$1_$2").toLowerCase()
}
/**
* @return model of null if no test could be created for this element
*/
fun createByElement(element: PsiElement): PyTestCreationModel? {
if (PythonUnitTestUtil.isTestElement(element, null)) return null //Can't create tests for tests
val file = element.containingFile as? PyFile ?: return null
val pyClass = PsiTreeUtil.getParentOfType(element, PyClass::class.java, false)
val function = PsiTreeUtil.getParentOfType(element, PyFunction::class.java, false)
val elementsToTest: Sequence<PsiNamedElement> = when {
function != null -> listOf(function)
pyClass != null -> pyClass.methods.asList()
else -> (file.topLevelFunctions + file.topLevelClasses) as List<PsiNamedElement>
}.asSequence().filterNot { PythonUnitTestUtil.isTestElement(it, null) }
val fileUnderTest = element.containingFile as? PyFile ?: return null
val classUnderTest = PsiTreeUtil.getParentOfType(element, PyClass::class.java, false)
val functionUnderTest = PsiTreeUtil.getParentOfType(element, PyFunction::class.java, false)
val elementsUnderTest: Sequence<PsiNamedElement> = when {
functionUnderTest != null -> listOf(functionUnderTest)
classUnderTest != null -> classUnderTest.methods.asList()
else -> (fileUnderTest.topLevelFunctions + fileUnderTest.topLevelClasses)
}.asSequence().filterIsInstance<PsiNamedElement>().filterNot { PythonUnitTestUtil.isTestElement(it, null) }
val functionNames = elementsToTest
.filterNot { it.name?.startsWith("__") == true }
/**
* [PyTestCreationModel] has optional field "class" and list of methods.
* For unitTest we need "class" field to filled by test name.
* For pytest we may leave it empty, but we need at least one method.
*/
val testFunctionNames = elementsUnderTest
.mapNotNull { it.name }
.map { "test_${it.toLowerCase()}" }.toList()
.filterNot { it.startsWith("__") }
.map { "test_${it.asFunName}" }.toMutableList()
return if (functionNames.isEmpty()) null
else {
val className = if (PythonUnitTestUtil.isTestCaseClassRequired(file)) "Test${pyClass?.name ?: ""}" else ""
PyTestCreationModel(fileName = "test_${file.name}",
targetDir = getTestFolder(element).path,
className = className,
methods = functionNames)
val nameOfClassUnderTest = classUnderTest?.name
// True for unitTest
val testCaseClassRequired = PythonUnitTestUtil.isTestCaseClassRequired(fileUnderTest)
if (testFunctionNames.isEmpty()) {
when {
// No class, no function, what to generate?
classUnderTest == null -> return null
// For UnitTest we can generate test class. For pytest we need at least one function
!testCaseClassRequired -> testFunctionNames.add("test_" + (nameOfClassUnderTest?.asFunName ?: "fun"))
}
}
return PyTestCreationModel(fileName = "test_${fileUnderTest.name}",
targetDir = getTestFolder(element).path,
className = if (testCaseClassRequired) "Test${nameOfClassUnderTest ?: ""}" else "",
methods = testFunctionNames)
}
@@ -9,3 +9,6 @@ class Spam:
def test_foo():
pass
class SpamSpamSpamBakedBeans:
pass
@@ -3,15 +3,14 @@ package com.jetbrains.python.codeInsight.testIntegration
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.WriteAction
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.psi.PsiElement
import com.intellij.testFramework.VfsTestUtil
import com.jetbrains.python.PyNames
import com.jetbrains.python.fixtures.PyTestCase
import com.jetbrains.python.psi.PyFile
import com.jetbrains.python.testing.PyTestFrameworkService
import com.jetbrains.python.testing.PythonTestConfigurationsModel
import com.jetbrains.python.testing.TestRunnerService
import org.junit.Assert
class PyTestCreationModelTest : PyTestCase() {
private val dir get() = myFixture.file.containingDirectory.virtualFile
@@ -19,59 +18,75 @@ class PyTestCreationModelTest : PyTestCase() {
private val service: TestRunnerService get() = TestRunnerService.getInstance(myFixture.module)
private val testsFolderName = "tests"
fun testWithUnitTest() {
service.projectConfiguration = PythonTestConfigurationsModel.PYTHONS_UNITTEST_NAME
val modelToTestClass = getModel(true)!!
Assert.assertEquals("test_create_tst.py", modelToTestClass.fileName)
Assert.assertEquals("TestSpam", modelToTestClass.className)
Assert.assertEquals(dirPath, modelToTestClass.targetDir)
Assert.assertEquals(modelToTestClass.methods, listOf("test_eggs", "test_eggs_and_ham"))
val modelToTestFunction = getModel(false)!!
Assert.assertEquals("test_create_tst.py", modelToTestFunction.fileName)
Assert.assertEquals("Test", modelToTestFunction.className)
Assert.assertEquals(dirPath, modelToTestClass.targetDir)
Assert.assertEquals(modelToTestFunction.methods, listOf("test_test_foo"))
}
fun testWithPyTest() {
service.projectConfiguration = PyTestFrameworkService.getSdkReadableNameByFramework(PyNames.PY_TEST)
val modelToTestClass = getModel(true)!!
Assert.assertEquals("test_create_tst.py", modelToTestClass.fileName)
Assert.assertEquals("", modelToTestClass.className)
Assert.assertEquals(dirPath, modelToTestClass.targetDir)
Assert.assertEquals(modelToTestClass.methods, listOf("test_eggs", "test_eggs_and_ham"))
Assert.assertNull("test_foo is test from pytest point of view, can't test it", getModel(false))
}
fun testTestFolderDetected() {
ApplicationManager.getApplication().invokeAndWait {
WriteAction.runAndWait<Throwable> {
VfsUtil.createDirectoryIfMissing(dir, testsFolderName)
}
}
val modelToTestClass = getModel(true)!!
Assert.assertEquals(dir.findChild(testsFolderName)!!.path, modelToTestClass.targetDir)
}
override fun setUp() {
super.setUp()
myFixture.configureByFile("/create_tests/create_tst.py")
}
fun testWithUnitTest() {
service.projectConfiguration = PythonTestConfigurationsModel.PYTHONS_UNITTEST_NAME
val modelToTestClass = getModel()!!
assertEquals("test_create_tst.py", modelToTestClass.fileName)
assertEquals("TestSpam", modelToTestClass.className)
assertEquals(dirPath, modelToTestClass.targetDir)
assertEquals(modelToTestClass.methods, listOf("test_eggs", "test_eggs_and_ham"))
val modelToTestFunction = getModelForFunc()!!
assertEquals("test_create_tst.py", modelToTestFunction.fileName)
assertEquals("Test", modelToTestFunction.className)
assertEquals(dirPath, modelToTestClass.targetDir)
assertEquals(modelToTestFunction.methods, listOf("test_test_foo"))
val modelToTestEmptyClass = getModel("SpamSpamSpamBakedBeans")!!
assertEquals("test_create_tst.py", modelToTestEmptyClass.fileName)
assertEquals("TestSpamSpamSpamBakedBeans", modelToTestEmptyClass.className)
assertEquals(dirPath, modelToTestEmptyClass.targetDir)
assertEquals(modelToTestEmptyClass.methods, emptyList<String>())
}
fun testWithPyTest() {
service.projectConfiguration = PyTestFrameworkService.getSdkReadableNameByFramework(PyNames.PY_TEST)
val modelToTestClass = getModel()!!
assertEquals("test_create_tst.py", modelToTestClass.fileName)
assertEquals("", modelToTestClass.className)
assertEquals(dirPath, modelToTestClass.targetDir)
assertEquals(modelToTestClass.methods, listOf("test_eggs", "test_eggs_and_ham"))
assertNull("test_foo is test from pytest point of view, can't test it", getModelForFunc())
val modelToTestEmptyClass = getModel("SpamSpamSpamBakedBeans")!!
assertEquals("test_create_tst.py", modelToTestEmptyClass.fileName)
assertEquals("", modelToTestEmptyClass.className)
assertEquals(dirPath, modelToTestEmptyClass.targetDir)
assertEquals(modelToTestEmptyClass.methods, listOf("test_spam_spam_spam_baked_beans"))
}
fun testTestFolderDetected() {
ApplicationManager.getApplication().invokeAndWait {
WriteAction.runAndWait<Throwable> {
VfsTestUtil.createDir(dir, testsFolderName)
}
}
val modelToTestClass = getModel()!!
assertEquals(dir.findChild(testsFolderName)!!.path, modelToTestClass.targetDir)
}
override fun tearDown() {
ApplicationManager.getApplication().invokeAndWait {
WriteAction.runAndWait<Throwable> {
dir.findChild(testsFolderName)?.delete(this)
dir.findChild(testsFolderName)?.let { VfsTestUtil.deleteFile(it) }
}
}
super.tearDown()
}
private fun getModel(forClass: Boolean): PyTestCreationModel? {
private fun getModelForFunc() = getModel(null)
private fun getModel(forClass: String? = "Spam"): PyTestCreationModel? {
val pyFile = myFixture.file as PyFile
val element: PsiElement = if (forClass) pyFile.topLevelClasses[0] else pyFile.findTopLevelFunction("test_foo")!!
val element: PsiElement = when {
forClass != null -> pyFile.findTopLevelClass(forClass)!!
else -> pyFile.findTopLevelFunction("test_foo")!!
}
return PyTestCreationModel.createByElement(element)
}
}