-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathREADME.in
364 lines (299 loc) · 10 KB
/
README.in
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
## ervilla
A minimalist JUnit 5 extension to create and destroy Podman containers
during tests.
### Features
* Conveniently and reliably start containers using `podman` in test suites.
* Programmable readiness checks for checking if a container is really ready for use.
* Written in pure Java 21.
* [OSGi](https://www.osgi.org/) ready.
* [JPMS](https://en.wikipedia.org/wiki/Java_Platform_Module_System) ready.
* ISC license.
* High-coverage automated test suite.
### Motivation
Test suites often contain integration tests that have dependencies on
external servers such as databases. Projects such as
[testcontainers](https://testcontainers.com/) provide a heavyweight and
complex API for starting Docker containers in tests, but no simple, lightweight
alternative exists for [Podman](https://podman.io).
The `ervilla` package provides a tiny API and a [JUnit 5](https://junit.org/junit5/)
extension for succinctly and efficiently creating (and automatically destroying)
Podman containers during tests.
### Building
```
$ mvn clean verify
```
### Usage
Annotate your test suite with `@ExtendWith(ErvillaExtension.class)`. This
will allow tests to get access to an injected `EContainerSupervisorType`
instance that can be used to create containers.
#### Container Database
`ervilla` maintains a persistent store of the containers and pods that it
has created. Each time the `ervilla` package is started, it will attempt to
clean up any pods and/or containers that have inadvertently managed to survive
the previous test run. The `ervilla` package expects each test suite to
provide a _project name_; A test run in a given project will _only_ attempt
to clean up containers/pods that are labelled with the same project name.
This is important with regard to concurrency: `ervilla` is designed to be
used in sets of projects that may be built in parallel on the same physical
machine during continuous integration runs. By keeping containers strictly
separated by project name, the package avoids accidentally trying to clean up
the containers that may be created by the _other_ project's test suite running
in parallel with the current project.
#### Disable Support
It can be useful to have tests simply disable themselves rather than running
and failing if they are executed on a system that doesn't have a `podman`
executable. Both the name of the `podman` executable and a flag
that disables tests if `podman` isn't supported can be specified in an
`@ErvillaConfiguration` annotation placed on the test class.
An example test from the test suite:
```
@ExtendWith(ErvillaExtension.class)
@ErvillaConfiguration(
projectName = "com.io7m.example"
podmanExecutable = "podman",
disabledIfUnsupported = true
)
public final class ErvillaExtensionContainerPerTest
{
private EContainerType container;
@BeforeEach
public void setup(
final EContainerSupervisorType supervisor)
throws Exception
{
this.container =
supervisor.start(
EContainerSpec.builder(
"quay.io",
"io7mcom/idstore",
"1.1.0"
)
.setImageHash(
"sha256:e77ad1f7f606a42a6bb0bbc885030a647fa4b90c15d47d5b32f43f1c98475f6e")
.addPublishPort(new EPortPublish(
new EPortAddressType.All(),
51000,
51000,
TCP
))
.addPublishPort(new EPortPublish(
new EPortAddressType.All(),
51001,
51001,
TCP
))
.addEnvironmentVariable("ENV_0", "x")
.addEnvironmentVariable("ENV_1", "y")
.addEnvironmentVariable("ENV_2", "z")
.addArgument("help")
.addArgument("version")
.build()
);
}
@Test
public void test0()
{
assertTrue(this.container.name().startsWith("ERVILLA-"));
}
@Test
public void test1()
{
assertTrue(this.container.name().startsWith("ERVILLA-"));
}
@Test
public void test2()
{
assertTrue(this.container.name().startsWith("ERVILLA-"));
}
}
```
#### Container Scope
An injected _supervisor_ instance has one of the following scope values:
* `PER_SUITE`
* `PER_CLASS`
* `PER_TEST`
Containers created from a supervisor with `PER_SUITE` scope will be destroyed
at the end of the entire test suite run.
Containers created from a supervisor with `PER_CLASS` scope will be destroyed
at the end of the containing test class execution.
Containers created from a supervisor with `PER_TEST` scope will be destroyed
at the end of each test method.
To get a `PER_SUITE` scoped supervisor, annotate the injected supervisor
parameter with `@ErvillaCloseAfterSuite`.
To get a `PER_CLASS` scoped supervisor, annotate the injected supervisor
parameter with `@ErvillaCloseAfterClass`.
To get a `PER_TEST` scoped supervisor, do not annotate the injected
supervisor.
```
@ExtendWith(ErvillaExtension.class)
@ErvillaConfiguration(disabledIfUnsupported = true)
public final class ErvillaExtensionCloseAfterAllTest
{
private static EContainerType CONTAINER;
@BeforeAll
public static void beforeAll(
final @ErvillaCloseAfterClass EContainerSupervisorType supervisor)
throws Exception
{
CONTAINER =
supervisor.start(
EContainerSpec.builder(
"quay.io",
"io7mcom/idstore",
"1.1.0"
)
.addPublishPort(new EPortPublish(
new EPortAddressType.All(),
51000,
51000,
TCP
))
.addPublishPort(new EPortPublish(
new EPortAddressType.All(),
51001,
51001,
TCP
))
.addEnvironmentVariable("ENV_0", "x")
.addEnvironmentVariable("ENV_1", "y")
.addEnvironmentVariable("ENV_2", "z")
.addArgument("help")
.addArgument("version")
.build()
);
}
@Test
public void test0()
{
}
@Test
public void test1()
{
}
@Test
public void test2()
{
}
}
```
The container is created once in the `@BeforeAll` method and assigned to
a static field `CONTAINER`. Each of `test0()`, `test1()`, `test2()` can
use this container instance, and the instance itself will be destroyed when
the test class completes.
#### Container Reuse
It is common, in test suites, to instantiate heavyweight containers such
as databases just once and then reuse those containers throughout the entire
test suite. The reuse of a container (with appropriate code to drop and
recreate a database each time) can turn a ten minute test suite execution into
a one minute execution.
The following is a relatively safe way to achieve this:
```
class TestServices {
private static EContainerType DATABASE;
public static void resetDatabase(
final EContainerType container)
{
// Database-specific code here, to drop and recreate databases...
...
...
}
public static EContainerType createDatabase(
final EContainerSupervisorType supervisor)
{
// Database-specific code here, to create database containers...
...
...
}
public static EContainerType database(
final EContainerSupervisorType supervisor)
{
if (DATABASE == null) {
DATABASE = createDatabase(supervisor);
}
return DATABASE;
}
}
@ExtendWith({ErvillaExtension.class})
@ErvillaConfiguration(projectName = "com.io7m.example", disabledIfUnsupported = true)
class Test0 {
private static EContainerType DATABASE;
@BeforeAll
public static void setupOnce(
final @ErvillaCloseAfterSuite EContainerSupervisorType containers)
throws Exception
{
DATABASE = TestServices.database(supervisor);
TestServices.resetDatabase(DATABASE);
}
@Test
public void test0()
{
// Use database here
}
@Test
public void test1()
{
// Use database here
}
@Test
public void test2()
{
// Use database here
}
}
@ExtendWith({ErvillaExtension.class})
@ErvillaConfiguration(projectName = "com.io7m.example", disabledIfUnsupported = true)
class Test1 {
private static EContainerType DATABASE;
@BeforeAll
public static void setupOnce(
final @ErvillaCloseAfterSuite EContainerSupervisorType containers)
throws Exception
{
DATABASE = TestServices.database(supervisor);
TestServices.resetDatabase(DATABASE);
}
@Test
public void test0()
{
// Use database here
}
@Test
public void test1()
{
// Use database here
}
@Test
public void test2()
{
// Use database here
}
}
```
Assuming that `resetDatabase` can drop and recreate the container's
database, and `createDatabase` does the initial creation of the
database container, the above `Test1` and `Test0` classes will reuse
the exact same database container instance.
#### Readiness Checks
It is possible to provide implementations of the `EReadyCheckType`
interface for each container. A readiness check is a piece of code that
is executed in a loop until it returns a positive response, and is responsible
for performing some kind of image-specific check to determine if a container
is actually ready for use.
By default, with no readiness checks provided for a container, the
system will only consider a container to be ready for use when the
underlying `podman` executable says that the container is in state `Up`.
However, for some images, the container being in the `Up` state doesn't
necessarily mean that the container is actually ready for use, and this
is where readiness checks should be used.
A good example of this is the images for [PostgreSQL](https://www.postgresql.org).
The PostgreSQL server takes a few seconds to start up while it loads various
bits of configuration and database state from the disk. This means that,
despite the container being in the `Up` state, the database won't actually be
ready to service database requests for at least a few seconds after startup.
The `EPgReadyCheck` class is a PostgreSQL-specific
readiness check that repeatedly tries to open a JDBC connection to a created
PostgreSQL container, and the system won't consider the container to be
ready for use until a JDBC connection succeeds.
Other readiness checks exist, such as checks to attempt to connect to
a bound TCP/IP socket on the container.