How do you write automated tests for scripts?
This question popped in my head when I had to write a script that deletes data in my previous job. Sure I could just run a container in the background but I wanted the setup to be easy for my coworkers. I also said to myself that no way will I write “a simple script” that deletes data without writing tests for it.
Below is a simple example of how I approached the situation. In this example, I’m using Groovy 3, JUnit 5, and Testcontainers.
To get started, I used the following folder structure.
project root
├── build.gradle
├── scripts
│ └── bash-script-that-inserts-data.sh
└── test
├── groovy
│ └── TestingScriptsTest.groovy
└── resources
└── schema.sql
- The scripts directory contains the scripts under test.
- The test/groovy directory contains the tests classes written in Groovy.
- The test/resources directory contains supporting files.
This is not the standard Maven source directory structure but it is similar to what I had to work with at the time. This structure can be configured in Gradle with following:
sourceSets {
test {
groovy {
srcDirs("test/groovy")
}
resources {
srcDirs("test/resources")
}
}
}
Below is the dependency declaration in Gradle.
dependencies {
implementation 'org.codehaus.groovy:groovy-all:3.0.8'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
testImplementation "org.testcontainers:junit-jupiter:1.16.3"
testImplementation "org.testcontainers:postgresql:1.16.3"
testImplementation 'org.postgresql:postgresql:42.3.1'
}
Now that the setup is complete. Let’s get started with writing and testing a script.
How to execute a Bash script from Groovy
To execute a Bash script from Groovy, one can do the following:
@Test
@DisplayName("Assert bash script can be executed")
void simpleTest() {
Process process = "sh ./scripts/simple-bash-script.sh".execute()
println(process.text) // prints standard output
assertEquals(0, process.onExit().get().exitValue(), "Exit value should be zero.")
}
In this test, I’m only asserting that the exit value is zero.
Setting up Testcontainers
Below is how I setup Testcontainers.
@Testcontainers
class TestingScriptsTest {
private static final String POSTGRES_DOCKER_IMAGE = "postgres:11.6-alpine"
private static final String DB_NAME = "test_db"
private static final String DB_USERNAME = "user"
private static final String DB_PASSWORD = "pass"
private static final String DB_HOST = "localhost"
private static final int POSTGRES_PORT = 5432
private static final String INIT_SCRIPT_PATH = "schema.sql"
private static int mappedPort
private static Sql sql
@Container
private static final JdbcDatabaseContainer POSTGRESQL_CONTAINER = new PostgreSQLContainer(
DockerImageName.parse(POSTGRES_DOCKER_IMAGE))
.withDatabaseName(DB_NAME)
.withUsername(DB_USERNAME)
.withPassword(DB_PASSWORD)
.withInitScript(INIT_SCRIPT_PATH)
@BeforeAll
static void beforeAll() {
assert POSTGRESQL_CONTAINER.running
// this is to acquire the random port that testcontainer uses
mappedPort = POSTGRESQL_CONTAINER.getMappedPort(POSTGRES_PORT)
// instantiate the groovy SQL object.
// not Testcontainer related.
sql = new Sql(
new PGSimpleDataSource(
serverName: DB_HOST,
databaseName: DB_NAME,
user: DB_USERNAME,
password: DB_PASSWORD,
portNumber: mappedPort)
)
}
}
The setup is relatively straight forward although I should point out that setup for JUnit 4 is different.
I encountered two issues when I was setting this up at my previous work.
- First is I didn’t know about
getMappedPort()
. This is actually the port to be used to connect to the database instance. - Second is the schema I worked with at the time used dollar quotes (
$$
) which did not work withwithInitScript()
. At this time, there’s an open issue for this at Testcontainers' github. To workaround this, I had to include loading of the schema via a Bash script in the@BeforeAll
block.
@BeforeAll
static void beforeAll() {
// ...
Process process = "./test/resources/load-schema.sh $username $host $port $databaseName $password".execute()
// ...
}
The load-schema.sql
script contains the psql
command to create the schema.
Asserting changes made
Now that the database schema has been setup, it’s time to test.
For the test case below, I’m testing a script that uses psql
to insert data in the public.items
table.
@Test
@DisplayName("Assert script inserts data")
void insertTest() {
Process process = "sh ./scripts/bash-script-that-inserts-data.sh $DB_USERNAME $DB_HOST $mapperPort $DB_NAME $DB_PASSWORD".execute()
println(process.text) //prints standard output
assertEquals(0, process.onExit().get().exitValue(), "Exit value should be zero.")
assertEquals(1, sql.firstRow('SELECT COUNT(id) as num FROM items').num)
}
After the execution of the script is done, I assert that the exit value should be zero and that the public.items
table exactly has one entry.
Conclusion
This post is a simple example of how to test data manipulating scripts using Groovy and Testcontainers. Although I have not tried it yet, I believe one can also execute Ruby or Python scripts this way and not just Bash scripts.
Link to repository can be found on Github.
References
https://www.testcontainers.org/test_framework_integration/junit_5/