-
Notifications
You must be signed in to change notification settings - Fork 126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New API and DSL #78
New API and DSL #78
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -254,8 +254,135 @@ class MysqlSpec extends FlatSpec with ForAllTestContainer { | |
} | ||
``` | ||
|
||
## New API | ||
|
||
Starting from 0.34.0 version testcontainers-scala provides the new API. | ||
The main motivation points are in the [pull request](https://github.com/testcontainers/testcontainers-scala/pull/78). | ||
|
||
### `Container` and `ContainerDef` | ||
|
||
Docker containers are represented through the two different entities: | ||
1. `ContainerDef` — it's container definition. `ContainerDef` describes, how to build a container. | ||
You can think about it like about a container constructor, or dockerfile description. | ||
Usually, `ContainerDef` receives some parameters. | ||
`ContainerDef` has a `start()` method. It returns a started `Container`. | ||
2. `Container` — it's a started container. You can interact with it through its methods. | ||
For example, in the case of `MySQLContainer` you can get it's JDBC URL with `jdbcUrl` method. | ||
`Container` is the main entity for using inside tests. | ||
|
||
### Scalatest usage | ||
|
||
You can use one of the four traits: | ||
1. `TestContainerForAll` — will start a single container before all tests and stop after all tests. | ||
2. `TestContainerForEach` — will start a single container before each test and stop after each test. | ||
3. `TestContainersForAll` — will start multiple containers before all tests and stop after all tests. | ||
4. `TestContainersForEach` — will start multiple containers before each test and stop after each test. | ||
|
||
|
||
#### Single container in tests | ||
|
||
If you want to use a single container in your test: | ||
```scala | ||
class MysqlSpec extends FlatSpec with TestContainerForAll { | ||
|
||
// You need to override `containerDef` with needed container definition | ||
override val containerDef = MySQLContainer.Def() | ||
|
||
// To use containers in tests you need to use `withContainers` function | ||
it should "test" in withContainers { mysqlContainer => | ||
// Inside your test body you can do with your container whatever you want to | ||
assert(mysqlContainer.jdbcUrl.nonEmpty) | ||
} | ||
} | ||
``` | ||
|
||
Usage of `TestContainerForEach` is not different from the example above. | ||
|
||
#### Multiple containers in tests | ||
|
||
If you want to use multiple containers in your test: | ||
```scala | ||
class ExampleSpec extends FlatSpec with TestContainersForAll { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would it be possible to replace abstract type with a type parameter? class ExampleSpec extends FlatSpec with TestContainersForAll[MySQLContainer and PostgreSQLContainer] I think this way it would be less verbose There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At first test traits had type parameters, as you suggest. But I changed it to the type member. There are 3 reasons:
|
||
|
||
// First of all, you need to declare, which containers you want to use | ||
override type Containers = MySQLContainer and PostgreSQLContainer | ||
|
||
// After that, you need to describe, how you want to start them, | ||
// In this method you can use any intermediate logic. | ||
// You can pass parameters between containers, for example. | ||
override def startContainers(): Containers = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried to find a way to do this and I came to the conclusion that there is no need for this default behavior. First of all, you need to pass parameters inside the container's definitions. So, anyway, you need to call Let's think about it. Imagine, that we have some default way to start containers in a defined order. I can imagine API like that: class ExampleSpec extends FlatSpec with TestContainersForAll {
val containers = MySQLContainer.Def(paramsHere) and PostgreSQLContainer.Def(paramsHere)
// Tests here
} And it looks exactly like the current API! With all its advantages and disadvantages. The main problem with it — how will you express dependent containers? Looks like you need a code block with the initialization logic: class ExampleSpec extends FlatSpec with TestContainersForAll {
val containers = {
val c1 = Container1.Def()
val c2 = Container2.Def(c1.someParam)
c1 and c2
}
// Tests here
} But, this code will not work, because class ExampleSpec extends FlatSpec with TestContainersForAll {
val containers = {
val c1 = Container1.Def().start()
val c2 = Container2.Def(c1.someParam).start()
c1 and c2
}
// Tests here
} But you can't create a class ExampleSpec extends FlatSpec with TestContainersForAll {
def startContainers = {
// Initialization here
}
// Tests here
} But, how will you provide started containers to the user? It looks like you need some function that will know about startup logic. Like class ExampleSpec extends FlatSpec with TestContainersForAll {
def startContainers = {
// Initialization here
}
it should "test" in withContainers { c1 and c2 =>
// Test body
}
}
class ExampleSpec extends FlatSpec with TestContainersForAll {
override type Containers = Container1 and Container2
override def startContainers(): Containers = {
val c1 = Container1.Def().start()
val c2 = Container2.Def(c1.someParam).start
c1 and c2
}
it should "test" in withContainers { c1 and c2 =>
// Test body
}
} Also, if you don't have dependent containers, you can write a bit less verbosely: def startContainers() = Container1.Def().start() and Container2.Def().start() Looks decent for me. Actually, the reasoning above is the exact reasoning of this whole API. This is how I end up with this API. |
||
val container1 = MySQLContainer.Def().start() | ||
val container2 = PostgreSQLContainer.Def().start() | ||
container1 and container2 | ||
} | ||
|
||
// `withContainers` function supports multiple containers: | ||
it should "test" in withContainers { case mysqlContainer and pgContainer => | ||
// Inside your test body you can do with your containers whatever you want to | ||
assert(mysqlContainer.jdbcUrl.nonEmpty && pgContainer.jdbcUrl.nonEmpty) | ||
} | ||
|
||
} | ||
``` | ||
|
||
Usage of `TestContainersForEach` is not different from the example above. | ||
|
||
### `GenericContainer` usage | ||
|
||
To create a custom container, which is not built-in in the library, you need to use `GenericContainer`. | ||
|
||
For example, you want to create a custom nginx container: | ||
```scala | ||
class NginxContainer(port: Int, underlying: GenericContainer) extends GenericContainer(underlying) { | ||
// you can add any methods or fields inside your container's body | ||
def rootUrl: String = s"http://$containerIpAddress:${mappedPort(port)}/" | ||
} | ||
object NginxContainer { | ||
|
||
// In the container definition you need to describe, how your container will be constructed: | ||
case class Def(port: Int) extends GenericContainer.Def[NginxContainer]( | ||
new NginxContainer(port, GenericContainer( | ||
dockerImage = "nginx:latest", | ||
exposedPorts = Seq(port), | ||
waitStrategy = Wait.forHttp("/") | ||
)) | ||
) | ||
} | ||
``` | ||
|
||
### Migration from the classic API | ||
|
||
1. If you have custom containers created with the `GenericContainer`, add `ContainerDef` in the companion like this: | ||
```scala | ||
object MyCustomContainer { | ||
case class Def(/*constructor params here*/) extends GenericContainer.Def[MyCustomContainer]( | ||
new MyCustomContainer(/*constructor params here*/) | ||
) | ||
} | ||
``` | ||
2. If you are using `ForEachTestContainer`: | ||
1. If your test contains only one container, replace `ForEachTestContainer` with `TestContainerForEach` | ||
2. If your test contains multiple containers, replace `ForEachTestContainer` with `TestContainersForEach` | ||
3. If you are using `ForAllTestContainer`: | ||
1. If your test contains only one container, replace `ForAllTestContainer` with `TestContainerForAll` | ||
2. If your test contains multiple containers, replace `ForAllTestContainer` with `TestContainersForAll` | ||
4. Fix all compilation errors using compiler messages and examples above. | ||
|
||
If you have any questions or difficulties feel free to ask it in our [slack channel](https://testcontainers.slack.com/messages/CAFK4GL85). | ||
|
||
## Release notes | ||
|
||
* **0.34.0** | ||
* Added new, experimental API and DSL. | ||
The main motivation points are in the [pull request](https://github.com/testcontainers/testcontainers-scala/pull/78). | ||
Old API remains the same, so all your old code will continue to work. | ||
We will wait for the user's feedback about the new API. | ||
If it will be positive, eventually this API may replace the current API. | ||
You can find more information about the new API above. | ||
|
||
* **0.33.0** | ||
* TODO | ||
|
||
* **0.32.0** | ||
* TestContainers -> `1.12.1` | ||
* SBT -> `1.3.0` | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package com.dimafeng.testcontainers | ||
|
||
import com.dimafeng.testcontainers.lifecycle.Stoppable | ||
import org.testcontainers.lifecycle.Startable | ||
|
||
trait ContainerDef { | ||
|
||
type Container <: Startable with Stoppable | ||
|
||
protected def createContainer(): Container | ||
|
||
def start(): Container = { | ||
val container = createContainer() | ||
container.start() | ||
container | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
package com.dimafeng.testcontainers.lifecycle | ||
|
||
trait Stoppable extends AutoCloseable with Andable { | ||
|
||
def stop(): Unit | ||
|
||
override def close(): Unit = stop() | ||
} | ||
|
||
/** | ||
* Gives you a possibility to write `container1 and container2`. | ||
* | ||
* Used for tests DSL. | ||
*/ | ||
sealed trait Andable { | ||
|
||
def stop(): Unit | ||
|
||
def foreach(f: Stoppable => Unit): Unit = { | ||
this match { | ||
case and(head, tail) => | ||
head.foreach(f) | ||
tail.foreach(f) | ||
|
||
case stoppable: Stoppable => | ||
f(stoppable) | ||
} | ||
} | ||
|
||
} | ||
final case class and[A1 <: Andable, A2 <: Andable](head: A1, tail: A2) extends Andable { | ||
|
||
/** | ||
* Stopping all Andable elements in the reverse order | ||
*/ | ||
override def stop(): Unit = { | ||
tail.stop() | ||
head.stop() | ||
} | ||
} | ||
|
||
object Andable { | ||
implicit class AndableOps[A <: Andable](val self: A) extends AnyVal { | ||
def and[A2 <: Andable](that: A2): A and A2 = com.dimafeng.testcontainers.lifecycle.and(self, that) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still believe that main container class should be its definition.
to make it more intuitive
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I played with this a bit and I didn't come up with something reasonable enough. Going this way will make it much harder to maintain backward compatibility with the current API. In this pull request, I managed to maintain backward compatibility relatively easy.
Anyway, this is experimental. I suggest to try it and wait for feedback.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense. I think I need to play with the code on my local machine to get a better sense of the backward compatibility in this implementation.
Btw, do you know any good way to gather feedback from early adopters?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I definitely will promote these changes in the Russian speaking scala user group in the telegram. I know that some people there are using testcontainers.
Also, recently I spoke about testcontainers and this pull request in the scalalaz podcast. This episode is still not released though. We need to wait for it a few more days.
I hope it would be enough at the start.