spek
spek copied to clipboard
Something for cleaning up resources
Hello,
Sorry if the answer is easily available but I did look through docs and codebase.
I'm looking for something similar to Spocks' @AutoCleanup spock.lang.AutoCleanup
that can take care of cleaning up resources at the end of the test, does something like this make sense or is feasible in Spek?
Thanks,
+1
It's the first time i heard about @AutoCleanup
, interesting. For now you have to do it manually using the beforeEach
and afterEach
fixtures.
The PR #100 that I've just posted proposes a solution.
While waiting for it to be reviewed, here's a code that I use (which is a very lite port of said PR):
import org.jetbrains.spek.api.DescribeBody
import java.io.Closeable
import java.util.*
interface AssertionBody {
fun onCleanup(block: () -> Unit)
fun <T : Closeable?> T.autoCleanup(): T = apply { if (this != null) onCleanup { close() } }
fun <T : AutoCloseable?> T.autoCleanup(): T = apply { if (this != null) onCleanup { close() } } // Remove in JDK 6
}
private class SpekAssertionBody : AssertionBody {
val blocks = ArrayList<() -> Unit>()
override fun onCleanup(block: () -> Unit) { blocks += block }
}
private fun _runAssertions(assertions: AssertionBody.() -> Unit) {
val body = SpekAssertionBody()
try {
body.assertions()
}
finally {
var cleanupException: Exception? = null
for (block in body.blocks)
try {
block.invoke()
}
catch (e: Exception) {
if (cleanupException == null)
cleanupException = e
else
cleanupException.addSuppressed(e) // Remove in JDK 6
}
if (cleanupException != null)
throw cleanupException
}
}
fun DescribeBody.r_it(description: String, assertions: AssertionBody.() -> Unit) = it(description) { _runAssertions(assertions) }
fun DescribeBody.r_xit(description: String, assertions: AssertionBody.() -> Unit) = xit(description) { _runAssertions(assertions) }
fun DescribeBody.r_fit(description: String, assertions: AssertionBody.() -> Unit) = fit(description) { _runAssertions(assertions) }
// Remove in JDK 6
@Suppress("NOTHING_TO_INLINE", "PLATFORM_CLASS_MAPPED_TO_KOTLIN")
private inline fun Throwable.addSuppressed(exception: Throwable) = (this as java.lang.Throwable).addSuppressed(exception)
With this, you can use the resource cleanup system when using r_it
:
class TestSpecs : Spek({
describe("a group") {
it("should demonstrate autoCleanup") {
val connection = datasource.getConnection().autoCleanup()
/* ... Test of the connection ...*/
}
}
})
In this code, the connection
object will be closed whether or not the test succeeds.
Hey @SalomonBrys thanks for the PR, unfortunately there are some changes happening in the codebase (we are upgrading to JUnit 5) that will render #100 invalid. If you want to test it out checkout this branch https://github.com/JetBrains/spek/tree/junit5.
@raniejade I've proposed #101 that does the same thing, but for the branch junit5 ;)
@danhyun At Spek's current state, you should not use afterEach
to do resource cleanup as afterEach
will only be called if the test suceeds!
With #102 we can easily add an extension method for cleaning up resources.
fun <T: Closeable> Dsl.resource(mode: CachingMode = CachingMode.TEST: factory() -> T): Memoized<T> {
val memoized = memoized(mode, factory)
val resource by memoized
when(mode) {
CachingMode.TEST -> afterEach { resource.close() }
CachingMode.GROUP -> after { resource.close() }
}
return memoized
}
Now we can do something like:
class SomeSpec: Spek({
val reader by resource { ... }
it("do something") {
reader.read()
}
})
I am facing this issue as well for memoized values. My memoized value is actually creating a temp directory and populating it and when it is no longer needed in the test (when the cached value is set to null) I would like to delete the directory (without just relying on File.deleteOnExit).
It seems to me what needs to happen is that when declaring the memoized value in addition to the factory lambda we also need to be able to add a cleanup lambda that gets called just before the LifecycleAwareAdapter is going to set the cached value to null.
The problem is the common one of how to elegantly pass 2 lambdas to a function or create some kind of dsl for it.
Here is one idea. Currently the parameter to memoized is "factory: () -> T
". How about we use an extension lambda instead. Say we create an interface like this (feel free to rename all of this as appropriate):
interface LifecycleFactory<T>
{
fun T.whenDone(cleanup: T.()->Unit) : T
}
So the memoized parameter would become "factory: LifecycleFactory<T>.() ->
T"
The idea being that I could declare this:
val directory : File by memoized {
createAndFillDirectory()
.whenDone { deleteRecursively() }
}
LifecycleAwareAdapter would implement this interface and its implementation would store the lambda to be called in all the places that set cached to null (presumably by creating a new method that does the call and sets to null)
@dalewking wow, that's a great idea. I was looking into how to prevent passing another lambda argument.
I'm not sure this extension will work though (I'm on mobile), what's wrong with clearing resources in afterEachTest?
On Thu, Jun 22, 2017, 03:54 Ranie Jade Ramiso [email protected] wrote:
@dalewking https://github.com/dalewking wow, that's a great idea. I was looking into how to prevent passing another lambda argument.
— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/JetBrains/spek/issues/95#issuecomment-310245230, or mute the thread https://github.com/notifications/unsubscribe-auth/AA7B3FCfHdkSwdid_qN-qN-aZuMA6Q3Kks5sGbtBgaJpZM4IzBzw .
It's doable. Yeah that's one way to do it:
val directory: File by memoized {
createAndFillDirectory()
.whenDone { deleteRecursively() }
}
Works as (w/ #214 memoized will not reset before any afterEachTest
):
val directory: File by memoized {createAndFillDirectory() }
...
afterEachTest {
directory.deleteRecursively()
}
Depending on the CachingMode
you should either be using afterGroup
or afterEachTest
. It's prone to error compared to the proposed extension.
~~Heck whenDone
can be implemented using afterEachTest
.~~ Nvm it can cause multiple registration of an afterEachTest
block since memoized' factory
can be invoke multiple times depending on the CachingMode
.
interface SpecBody {
...
fun <T> T.whenDone(cleanup: T.() -> Unit) {
afterEachTest { cleanup(this) }
}
}
I would say the pros for my approach is that I don't have to remember which type of after method I have to use based on which scope I have chosen for the memoized. And in my opinion the way the scopes work is a little unintuitive and undocumented. It actually took some trial and error before I learned that Test scope and afterEachTest are not just limited to a test. Multiple tests within an Action works differently and the test scope then extends to the entire action. That is what I was looking for but I had to dig to figure that out.
With this approach you just say whenever this gets cleaned up do this.
Another pro is that there may be some logic in determining what the memoized resource is and depending on that logic you may not need to always do the cleanup as in:
val dir : File by memoized {
when {
shouldCreateDirectory -> createAndFillDirectory().whenDone { deleteRecursively }
else -> rootDirectory
}
}
The downsides on my approach is that you can have mutliple whenDone blocks. Do you support that and execute all of them or try to prevent that? You could prevent it by instead have the whenDone return some wrapper of T and you overload memoized to also support an extension on the wrapper of T.
I also don't like that it is an extension on T when that is just a convenience to support a fluent style. It could just be a plain function so that you would write it like this:
val directory: File by memoized {
whenDone { deleteRecursively() }
createAndFillDirectory()
}
It seems backwards though to specify the clean up before specifying what you are creating but it might make sense to support both styles.
Regarding the fact that it is an extension on T, it might make more sense to not limit this to just T. Sometimes creating the memoized resource is a chain, not a single step, and the thing you need to clean up is not necessarily the final product. Perhaps you make whenDone an extension on any type, not just T and the implementation stores the target and the lambda and at the appropriate time calls the lambda on the target so you can write this:
val bar : Bar by memoized {
val foo = createFoo().whenDone { doCleanupOnFoo() }
createBar(foo)
}
Of course that could be solved by making foo its own memoized value
I created spek-testfiles
, an extension that handles test files and directories. I think it fits this use case well as far as cleaning up the file system is concerned. If you create a directory with it, it will be cleaned up automatically.
import de.joshuagleitze.test.spek.testfiles.testFiles
import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe
object ExampleSpek : Spek({
val testFiles = testFiles()
describe("using test files") {
it("cleans up files") {
val testDir = testFiles.createDirectory() // will be deleted after the test if the test succeeds
fillDirectory(testDir)
// do some testing
}
}
})
By default, the directory will be retained if the test fails, but you can also choose different deletion modes.