TestDsl¶
interface TestDsl : PreparedDsl
A test declaration.
This interface is most often used as a test declaration: suspend TestDsl.() -> Unit.
Tests allow to control the time. For more information, read time.
Design notes¶
It is our goal to keep this interface as lightweight as possible, because any field we add here risks being shadowed by local variables in the tests.
For example, if we were to add a member called foo, then this code…
…shadows the member 'foo'.
Instead, we add all fields to TestEnvironment, and create extension functions which expose the most important functionality.
Note to runner implementors¶
If you are implementing your own test runner, you will need to provide an instance of this interface. Because it encapsulates the whole test machinery, we recommend using runTestDsl instead of making your own implementation.
See also¶
cleanUp: Register a finalizer which is executed at the end of the test
Properties¶
backgroundScope¶
CoroutineScope for services started by this test.
The test finishes when all tasks started in foregroundScope are finished. If there are still tasks running in backgroundScope, they are cancelled.
This is useful to execute background services which are not part of the system-under-test, yet are expected to be running by the system-under-test.
To start a single coroutines, see launchInBackground.
Tasks started in this scope respect the controlled time.
See also
environment¶
abstract val environment: TestEnvironment
Metadata about the running test.
foregroundScope¶
CoroutineScope for tasks started by this test.
The test will only finish when all tasks started in this scope are finished.
To start a single coroutine, see launch.
Tasks started in this scope respect the controlled time.
See also
Random¶
See random.
random¶
Random generator control center.
time¶
Time control center.
Why?¶
Often, we need to create algorithms that behave differently when executed at different times. Using the system clock makes code much harder to test, or problems to reproduce. Instead, it is recommended that algorithms take as input a "time generator", so fake implementations can be injected for testing.
-
When we want to measure elapsed time, we should inject a
TimeSource(seeTime.source). -
When we want to access the current time, we should inject some kind of Clock (e.g. the one provided by
java.time, or the one provided byKotlinX.Datetime, see the various extensions onTime).
When executing the system under test, we need to provide such objects to the algorithm. This attribute, time, is the control center for generating such values and for controlling their outputs.
Delay-skipping¶
Inside tests, calls to delay are skipped in a way that keeps the order of events. This makes tests much faster to execute without compromising on testing algorithms that need to wait for an event.
This also applies to other time-related coroutine control functions, like withTimeout.
This allows to trivially implement algorithms which require skipping a large amount of time:
test("Data is expired after 6 months") {
val data = createSomeData()
assertFalse(data.isExpired())
delay((6 * 30).days)
assertTrue(data.isExpired())
}
Assuming all services use either the test clock or the test time source, the entire system will think 6 months have passed, and all started tasks will have run the same number of times, and in the same order, as if 6 months had actually passed.
To learn more about delay skipping, see the KotlinX.Coroutines' documentation: runTest.
The delay-skipping behavior is controlled by
Time.scheduler. If you want to create your own coroutines, remember to add the scheduler to theirCoroutineContext, or they will delay for real.
Time control¶
Inside tests, a virtual time is available, that describes how much delay has been skipped. A test always starts at the epoch. delay allows us to move time forwards, executing all tasks as their execution date is reached. We can also control the time directly.
-
Time.nowMillis: Access the current time in milliseconds (see the variousnow*accessors). -
Time.advanceByMillis,Time.advanceBy: Advance the current time without executing the awaiting tasks. -
Time.advanceUntilIdle: Advance the current time, executing all tasks in order, until all tasks have been executed. -
Time.runCurrent: Run all tasks enqueued for the current time.
Example¶
This example checks that an event was recorded at the expected time:
test("The event should be recorded at the current time") {
val start = time.nowMillis
val result = foo()
val end = time.nowMillis
assert(result.timestamp > start)
assert(result.timestamp < end)
}
See also
-
Time.source: Measure elapsed time -
nowMillis: Current time, in milliseconds
Functions¶
cleanUp¶
Registers a block named name to run at the end of the test.
The block will run even if the test fails.
Finalizers are ran in inverse order as their registration order.
Example¶
val prepareDatabase by prepared { FakeDatabase() }
test("Create a user") {
val database = prepareDatabase()
val user = database.createUser()
cleanUp("Delete user $user") {
database.deleteUser(user)
}
}
Declaration from within prepared values¶
This function can be called from within a prepared value, in which case it will run when the test that initialized that prepared value finishes:
val prepareDatabase by prepared {
FakeDatabase()
.also { cleanUp("Disconnect from the database") { it.disconnect() } }
}
val prepareUser by prepared {
val database = prepareDatabase()
database.createUser()
.also { cleanUp("Delete user $it") { database.deleteUser(it) } }
}
test("Rename a user") {
val user = prepareUser()
user.rename("New name")
// will automatically run:
// 1. Delete user …
// 2. Disconnect from the database
}
Parameters
-
onSuccess: If
false, this finalizer will not run if the test failed. -
onFailure: If
false, this finalizer will not run if the test was successful.
immediate¶
Realizes a Prepared value from the provided PreparedProvider.
Because the prepared value is created and used immediately, it cannot be saved to be reused—this means that it won't have the reuse behavior of Prepared; that is:
val prepareRandomInt = prepared { random.nextInt() }
val first by prepareRandomInt // bind to a Prepared instance
test("An example") {
assertEquals(first(), first()) // it is bound, so it always gives the same value
assertNotEquals(prepareRandomInt.immediate(), prepareRandomInt.immediate()) // it is unbound, so each call gives a new value
}
This function is mostly useful because test fixtures are often provided as PreparedProvider instance to benefit from the other features of this library. Sometimes, however, we just need a single value at a single point in time, which is why this function exists.
invoke¶
Realizes a Prepared value in the context of this test.
launch¶
@IgnorableReturnValue
fun TestDsl.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
Starts a task in the foregroundScope. The test will wait for this task before finishing.
By default, tasks started are run sequentially. To execute tasks in parallel, explicitly use a CoroutineDispatcher.
The task will respect the controlled time.
See also
launchInBackground¶
@IgnorableReturnValue
fun TestDsl.launchInBackground(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
Starts a task in the backgroundScope scope. The test will not wait for this task before finishing.
This is useful to start background services which are not part of the system-under-test, yet are expected to be running by the system-under-test.
By default, tasks started are run sequentially. To execute tasks in parallel, explicitly use a CoroutineDispatcher.
The task will respect the controlled time.
See also
log¶
Logs a value in the test output.
Logs a value in the test output, with some additionalInfo.