It's become commonplace to partially automate tests.
It seems that some people sometimes use Thread.sleep ()
to wait for the browser to work. wait a minute. Doesn't it change depending on the sleep time and execution environment? If you want to test the process of waiting for a fixed time, you can use Thread.sleep ()
. However, I think that the sleep time is adjusted on an ad hoc basis, such as "It depends on the network how long it will take, but it will not work unless you wait for about 2 seconds."
I wrote earlier How to wait for screen drawing in Selenium. At this time, I wanted to use normally {} that was in ScalaTest in Java, so I implemented it in Java. I want to use it in kotlin this time, no, I can call Java classes and methods from kotlin, but I think if I write it in kotlin, it will be more beautiful. I also use vavr. Unlike the last time, let's see how to flesh out after writing the basic part.
Here, instead of the Java / Kotlin interface, we will consider what kind of form annually should be easy to use. When you use it, you want to feel free to use it like this.
driver.get(url)
eventually {
//It takes time to open the url, so an error will occur immediately after that.
assert(driver.findElementById("id"))
}
Let's just allow the parentheses to increase when specifying the deadline and waiting time. You can prepare a default timeout, usually without parentheses, and specify it only when you absolutely need to specify it.
broswer.login()
eventually ({
assert(browser.contents().contains(data))
}, Duration.ofSeconds(2), Duration.ofSeconds(1))
Since kotilin has a default argument, you don't have to use it to create a similar method like Java with different arguments.
object Eventually {
val DEFAULT_TIMEOUT = Duration.ofSeconds(10)
val DEFAULT_INTERVAL = Duration.ofSeconds(3)
fun <R> eventually(f: () -> R, timeout: Duration = DEFAULT_TIMEOUT, interval: Duration = DEFAULT_INTERVAL): R {
...
However, this interface required both parentheses and curly braces, as follows, even without a timeout.
eventually ({
// ...
})
I can't help it, so I'll prepare another one.
fun <R> eventually(f: () -> R) = Eventually.eventually(f, DEFAULT_TIMEOUT, DEFAULT_INTERVAL)
fun <R> eventually(f: () -> R, timeout: Duration = DEFAULT_TIMEOUT, interval: Duration = DEFAULT_INTERVAL): R {
...
You can now call either of the following:
eventually { ... }
eventually ({ ... }, timeout, interval)
...
Of course, since it is kotlin, you can also specify the argument name.
//I want to specify only interval
eventually ({ ... }, interval=Duration.ofSeconds(1))
...
First, consider a function that "processes f () and retries if an exception occurs".
fun <R> tryExecute(f: () -> R):R {
return try {
f()
} catch (t: Throwable) {
tryExecute(f)
}
}
Now you have a function that loops forever as long as you keep getting errors. Let's set a deadline because it is a problem if it does not stop forever. Here we use java.time.Instant. I want to specify the timeout in Duration, but I will explain it later. Why should I add tailrec to the recursion?
tailrec fun <R> tryExecute(f: () -> R, until: Instant):R {
if(now()>until) throw RuntimeException("I can not do it")
return try {
f()
} catch (t: Throwable) {
Thread.sleep(interval)
tryExecute(f, until)
}
}
If f () throws an exception immediately, one CPU (all depending on the implementation of f ()) will be used up until the deadline, and if an error occurs, it will wait for a certain period of time. This time we will use java.time.Duration to represent the period.
tailrec fun <R> tryExecute(f: () -> R, until: Instant, interval: Duration):R {
if(now()>until) throw RuntimeException("I can not do it")
return try {
f()
} catch (t: Throwable) {
Thread.sleep(interval)
tryExecute(f, until, interval)
}
}
Now, you want the exception that f () throws. Required for debugging. Let's pass the exception that occurred when recursing and return it as the cause of the exception when it finally times out. The argument definition is getting longer, so I'll make it one character.
tailrec fun <R> tryExecute(f: () -> R, u: Instant, i: Duration, t: Throwable):R {
if(now() > u) throw RuntimeException("I can not do it", t)
return try {
f()
} catch (t: Throwable) {
Thread.sleep(interval)
tryExecute(f, u, i, t)
}
}
Now when it times out, cause () will pick up the last caught exception. The logic is now complete.
Let's call the current logic from the method we just created. Calculate the expiration date by adding the timeout given by Duration to the current time.
val start = Instant.now()
val until = start.plusMillis(timeout.toMillis())
tryEvent takes the last caught exception as an argument, but at first no exception occurs and it is unpleasant to pass null, so set Option
tailrec fun <R> tryExecute(f: () -> R, u: Instant, i: Duration, t: Option<Throwable>): R {
if (Instant.now() > u) throw t.getOrElse(TimeoutException())
return try {
f()
} catch (t: Throwable) {
Thread.sleep(i.toMillis())
tryExecute(f, u, i, Option.some(t))
}
}
tryExecute(f, until, interval, Option.none())
TimeoutException is thrown when a timeout occurs before the exception occurs even once. So, when you catch this exception, create an error message along with the elapsed time.
//Caller
try {
tryExecute(f, until, interval, Option.none())
} catch (t: Throwable) {
throw createException(start, t)
}
Message composition function. replace will convert to value if there is a key in map for string s. It's recursive, but you can fold it with map.fold.
fun replace(s: String, m: Map<String, String>): String =
if (m.isEmpty) s else replace(s.replace(":" + m.head()._1, m.head()._2), m.tail())
private fun createException(start: Instant, t: Throwable): Throwable {
val messageMap = HashMap.ofEntries<String, String>(
Tuple2("time", Duration.between(start, Instant.now()).toString()),
Tuple2("message", t.message)
)
return RuntimeException(replace(MESSAGE_TEMPLATE, messageMap), t)
}
That's all there is to it. The whole thing is like this.
Eventually.kt
import io.vavr.Tuple2
import io.vavr.collection.HashMap
import io.vavr.collection.Map
import io.vavr.control.Option
import java.time.Duration
import java.time.Instant
import java.util.concurrent.TimeoutException
object Eventually {
val DEFAULT_TIMEOUT = Duration.ofSeconds(10)
val DEFAULT_INTERVAL = Duration.ofSeconds(3)
val MESSAGE_TEMPLATE = "Eventually failed over :time. Last message is:\n:message";
internal tailrec fun replace(s: String, m: Map<String, String>): String =
if (m.isEmpty) s
else replace(s.replace(":" + m.head()._1, m.head()._2), m.tail())
private tailrec fun <R> tryExecute(f: () -> R, u: Instant, i: Duration, t: Option<Throwable>): R {
if (Instant.now() > u) throw t.getOrElse(TimeoutException())
return try {
f()
} catch (t: Throwable) {
Thread.sleep(i.toMillis())
tryExecute(f, u, i, Option.some(t))
}
}
private fun createException(start: Instant, t: Throwable): Throwable {
val messageMap = HashMap.ofEntries<String, String>(
Tuple2("time", Duration.between(start, Instant.now()).toString()),
Tuple2("message", t.message)
)
return RuntimeException(replace(MESSAGE_TEMPLATE, messageMap), t)
}
fun <R> eventually(f: () -> R) = Eventually.eventually(f, DEFAULT_TIMEOUT, DEFAULT_INTERVAL)
fun <R> eventually(f: () -> R, timeout: Duration = DEFAULT_TIMEOUT, interval: Duration = DEFAULT_INTERVAL): R {
val start = Instant.now()
val until = start.plusMillis(timeout.toMillis())
return try {
tryExecute(f, until, interval, Option.none())
} catch (t: Throwable) {
throw createException(start, t)
}
}
At first, I tried to use io.vavr.kotlin.Try without using try {}, and tried and errored, and triedEvent () returned Either <R, Throwable>, but this one I changed it because it was cleaner. However, the number of lines has not decreased much compared to Java version. Please let me know if there is a better way.
Actually, we will test each function while implementing it. For example, when you write replace (), it checks if that part works as intended. Since replace can be private, you can delete testReplace () after confirming that replace () is called in the eventually test. In that case, change ʻinternal fun replace ()to
private fun replace ()`. I'm leaving it here.
EventuallyTest.kt
import <src>.Eventually.eventually
import <src>.Eventually
import io.vavr.Tuple2
import io.vavr.collection.HashMap
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Test
import org.slf4j.LoggerFactory
import java.lang.IllegalArgumentException
import java.time.Duration
typealias e = Tuple2<String, String>
class EventuallyTest {
private val log = LoggerFactory.getLogger(this.javaClass)
@Test
fun testReplace() {
val r = Eventually.replace("this is :1, :2, :x", HashMap.ofEntries(
e("1", "changed"),
e("2", "zzzzz"),
e("x", "yyyyy")
))
log.info(r)
assertEquals("this is changed, zzzzz, yyyyy", r)
}
@Test
fun testEventually() {
val r = eventually {
log.info("aaa")
"a"
}
assertEquals("a", r)
}
@Test
fun testEventually2Sec() {
try {
eventually({
log.info("aaa")
throw IllegalArgumentException("x")
}, timeout = Duration.ofSeconds(2))
}catch (e: Exception){
assertEquals("x", e.cause!!.message)
}
}
}
KotlinTest
Apparently KotlinTest [eventually](https://github.com/kotlintest/kotlintest/blob/master/kotlintest-assertions/src/jvmMain/kotlin/io/kotlintest/ There seems to be Eventually.kt). There is also FunSpec etc. and it seems convenient to use like ScalaTest. The package relationship of KotlinTest itself is complicated and the hurdle seems to be high, but if it can be introduced, this is also good. There is also an extension method for arrow.
Recommended Posts