docs/src/main/sphinx/develop/tests.md
The Trino project aims to provide high quality artifacts and features to all users. A suite of tests used for development, continuous integration builds, and releases is critical for achieving this goal.
Trino acts as an integration layer for many different data sources with the help of different connector plugin, and includes support of other plugins as well. This results in the need testing many different use cases with a number of external systems. The Trino test suite limits the executed tests for any changes in pull requests as much as possible, while any merges to the master branch process all tests.
The following requirements and guidelines help contributors to create tests with the following goals:
The following section details conventions and recommendations to follow when creating new tests or refactoring existing test code. The preferred approaches The existing codebase is a mixture of newer test code that adheres to these guidelines and older legacy code. The legacy test code should not be used as example for new tests, rather follow the guidelines in this document.
Also note that the guidelines are subject to change in a process of further refinement and improvements from practical experience.
A number of requirements apply to all new tests, and any refactoring work of existing tests:
org.assertj.core.api.Assertions.Test, for example TestExampletest, for example testExplain()In addition to the requirements, a number of specific guidelines help with authoring new tests as well as refactoring existing tests.
Testing in Trino is extremely expensive, and slows down all development as they take hours of compute time in a limited environment. For large expensive tests, consider the value the test brings to Trino, and ensure the value is justified by the cost. We effectively have a limited budget for testing, and CI tests queue on most days, often for many hours, which reduces the overall project velocity.
Prefer tests of items in isolation and test a few common combinations to verify integrations are functional. Do not implement tests for all possible combinations.
If you can create a unit test for a feature, use a unit test and avoid writing a product test. Over time the aim is to remove the majority of product tests, and avoiding new product tests helps to prevent the migration costs from growing.
Only use product tests in the following cases:
The following approaches should be avoided because the existing build tools and frameworks provide sufficient capabilities:
Data providers and parametric tests add unnecessary complexity. Consider focusing on high value tests and avoiding combinatorial tests, and the following details:
Stateful tests can lead to issues from on one test leaking into other tests, especially when test runs are parallelized. As a result debugging and troubleshooting test failures and maintenance of the tests is more difficult. If possible these stateful test classes should be avoided.
JUnit and the JVM take care of test life cycle and memory management. Avoid
manual steps such as nulling out fields in @After methods to “free memory”. It
is safe to assign memory intensive objects to final fields, as the class is
automatically dereferenced after the test run.
Prefer resource initialization in constructors and tear them down in @After
methods if necessary. This approach, combined with not nulling fields, allows
the fields to be final and behave like any Closeable class in normal Java code
Consider using the Guava Closer class to simplify cleanup.
Avoid the @Before/@After each test method style of setup and teardown.
New plugin/connector features should be testable using one of the testing plugins (e.g., memory or null). There are existing features only tested in plugins in Hive, and over time we expect coverage using the testing plugins
For plugins and specifically connector plugins, focus on the code unique to the plugin. Do not add tests for core engine features. Plugins should be focused on the correctness of the SPI implementation, and compatibility with external systems.
Flaky tests are test that are not reliable. Multiple runs of the same test result in inconsistent results. Typically the tests are successful, and then rarely fail. Reasons for flakiness include reliance on external, unstable systems, connections, and other hard to troubleshoot setups.
Existing flaky tests using the legacy TestNG library can be marked with the
@Flaky annotation temporarily to improve CI reliability until a fix is
implemented:
After a certain time period, if the test hasn’t been fixed, it should be removed.
New tests with the @Flaky annotation can not be introduced, since new tests
must use JUnit. Rewrite the test to be stable or avoid the test altogether.
Prefer to remove a test instead of disabling it. Test code is maintained and updated as the codebase changes, and inactive tests just waste time and effort.
Disabled tests can be removed at any time.
Assumptions.abort()The approach to use Assumptions.abort() to skip a test, especially deep in the
call stack, makes it difficult to debug tests failures. The abort() works by
throwing an exception, which can be caught by intervening code inadvertently,
leading to misleading stack traces and test failures.
Inheritance of tests creates unnecessary complexity. Keep tests simple and use composition if necessary.
The required usage of AssertJ provides a rich set of assertions, that typically makes custom helper assertions unnecessary. Custom assertions often make tests harder to follow and debug.
If you decide a helper assertion is needed, consider the following details:
assert, for example assertSomeLogicWorksThe following examples showcase recommended and discourage practices.
Use PER_CLASS for instances because QueryAssertions is too expensive to
create per-method, and a allow parallel execution of tests with CONCURRENT:
@TestInstance(PER_CLASS)
@Execution(CONCURRENT)
final class TestJoin
{
private final QueryAssertions assertions = new QueryAssertions();
@AfterAll
void teardown()
{
assertions.close();
}
@Test
void testXXX()
{
assertThat(assertions.query(
"""
...
"""))
.matches("...");
}
}
Avoid managing the lifecycle of a Closeable like a connection with
@BeforeEach/@AfterEach to reduce overhead:
@TestInstance(PER_METHOD)
final class Test
{
private Connection connection;
@BeforeEach
void setup()
{
// WRONG: create this in the test method using try-with-resources
connection = newConnection();
}
@AfterEach
void teardown()
{
connection.close();
}
@Test
void test()
{
...
}
}
Using a try with resources approach allows clean parallelization of tests and includes automatic memory management:
final class Test
{
@Test
void testSomething()
{
try (Connection connection = newConnection();) {
...
}
}
@Test
void testSomethingElse()
{
try (Connection connection = newConnection();) {
...
}
}
}
Avoid using fake abstraction for tests.
@DataProvider(name = "data")
void test(boolean flag)
{
// WRONG: use separate test methods
assertEqual(
flag ? ... : ...,
flag ? ... : ...);
}
Replace with simplified separate assertions:
void test()
{
assertThat(...).isEqualTo(...); // case corresponding to flag == true
assertThat(...).isEqualTo(...); // case corresponding to flag == false
}
Do not develop a custom parallel test execution framework:
@Test(dataProvider = "parallelTests")
void testParallel(Runnable runnable)
{
try {
parallelTestsSemaphore.acquire();
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
try {
runnable.run();
}
finally {
parallelTestsSemaphore.release();
}
}
@DataProvider(name = "parallelTests", parallel = true)
Object[][] parallelTests()
{
return new Object[][] {
parallelTest("testCreateTable", this::testCreateTable),
parallelTest("testInsert", this::testInsert),
parallelTest("testDelete", this::testDelete),
parallelTest("testDeleteWithSubquery", this::testDeleteWithSubquery),
parallelTest("testUpdate", this::testUpdate),
parallelTest("testUpdateWithSubquery", this::testUpdateWithSubquery),
parallelTest("testMerge", this::testMerge),
parallelTest("testAnalyzeTable", this::testAnalyzeTable),
parallelTest("testExplainAnalyze", this::testExplainAnalyze),
parallelTest("testRequestTimeouts", this::testRequestTimeouts)
};
}
Leave parallelization to JUnit instead, and implement separate test methods instead.
Do not create a custom parameterized test framework:
@Test
void testTinyint()
{
SqlDataTypeTest.create()
.addRoundTrip(...)
.addRoundTrip(...)
.addRoundTrip(...)
.execute(getQueryRunner(), trinoCreateAsSelect("test_tinyint"))
.execute(getQueryRunner(), trinoCreateAndInsert("test_tinyint"))
.addRoundTrip(...)
.execute(getQueryRunner(), clickhouseQuery("tpch.test_tinyint"));
}