Proposal: Separating Integration Tests

Separating Integration Tests

Current problem

For a historical reason, the majority of TiDB ‘unit test’ cases are coded as integration tests. Since it is simple and quick, a lot of examples can be found everywhere. Here is 1 simple example:

executor/aggregate_test.go

func (s *testSuiteAgg) TestAggregation(c *C) {
	tk := testkit.NewTestKit(c, s.store)
	tk.MustExec("set @@tidb_hash_join_concurrency=1")
	tk.MustExec("use test")
	tk.MustExec("set sql_mode='STRICT_TRANS_TABLES'") // disable only-full-group-by
	tk.MustExec("drop table if exists t")
	tk.MustExec("create table t (c int, d int)")
	tk.MustExec("insert t values (NULL, 1)")
	tk.MustExec("insert t values (1, 1)")
	tk.MustExec("insert t values (1, 2)")
	tk.MustExec("insert t values (1, 3)")
	tk.MustExec("insert t values (1, 1)")
	tk.MustExec("insert t values (3, 2)")
	tk.MustExec("insert t values (4, 3)")
	tk.MustQuery("select bit_and(c) from t where NULL").Check(testkit.Rows("18446744073709551615"))
	tk.MustQuery("select bit_or(c) from t where NULL").Check(testkit.Rows("0"))
       ...
}

It brings us a lot of troubles:

  • The real unit tests are not designed and implemented.
  • It brings a lot of concurrent execution issues(like ‘DATA RACE’) because modifying internal variables is easy.

Goal

We would like to move those integration tests out of the codes.

Proposal

  1. Provide a new directory named ‘tidb-test’ in TiDB server codebase(pingcap/tidb), just like what MySQL did: mysql-test.
  2. Port as many as the ‘integration cases’ in ‘xxx_test.go’ to ‘tidb-test’ directory.

Take executor/aggregate_test.go(the example given earilier in this topic) to:

# $TIDB_SRC/tidb-test/t/aggregate_suite.test

# case: TestAggregation
set @@tidb_hash_join_concurrency=1    
use test    
set sql_mode='STRICT_TRANS_TABLES'    
drop table if exists t    
create table t (c int, d int)    
insert t values (NULL, 1)    
insert t values (1, 1)    
insert t values (1, 2)    
insert t values (1, 3)    
insert t values (1, 1)    
insert t values (3, 2)    
insert t values (4, 3)    
select bit_and(c) from t where NULL    
select bit_or(c) from t where NULL       
...

# $TIDB_SRC/tidb-test/r/aggregate_suite.result
set @@tidb_hash_join_concurrency=1    
use test    
set sql_mode='STRICT_TRANS_TABLES'    
drop table if exists t    
create table t (c int, d int)    
insert t values (NULL, 1)    
insert t values (1, 1)    
insert t values (1, 2)    
insert t values (1, 3)    
insert t values (1, 1)    
insert t values (3, 2)    
insert t values (4, 3)    
select bit_and(c) from t where NULL
18446744073709551615    
select bit_or(c) from t where NULL  
0
  1. Update Makefile to trigger mysql-tester for make test.

The automatic ports

Porting the cases from the code to mysql-tester manually can be a very big amount of work. We need an automatic and quick way to archive it.

There are 2 major testing frameworks: pingcap/check and stretchr/testify. Most of the test cases can be generated automatically by hacking the framework.

For pingcap/check:

// Open the test suite file in when `SetUpSuite` is called and close the file when `TearDownSuite` is called:

f = os.Open(‘$SUITE_NAME/$TESTCASE_NAME’)
caseRunner.run(case.SetUpSuite())

// ...

caseRunner.run(case.TearDownSuite())
f.Close()

// Write the `sql` statement to test file when `tk.MustExec(sql)` and `tk.MustQuery(sql)` is called:

func (tk *TestKit) MustExec(sql string) {
        if tk.f != nil {
                _, _ = tk.f.WriteString(sql)
                _, _ = tk.f.WriteString("\n")
        }
}

func (tk *TestKit) MustQuery(sql string) {
        if tk.f != nil {
                _, _ = tk.f.WriteString(sql)
                _, _ = tk.f.WriteString("\n")
        }
}

Challenges

  • How to(or should we?) port the test cases with the failpoint.
  • Many test cases change the configuration of TiDB dynamically (setting some internal variables at the beginning and restoring it after the case is done), which is not practical for ‘mysql-tester’.
  • Performance: running integration tests on ‘mysql-tester’ will be slower compared to the current built-in code way.

Known risks

Since the ‘mysql-tester’ is our own implementation of ‘mysql-test’ (they look the same). It is likely that engineers may want to port test cases from MySQL source to TiDB server repository, which violates the license of MySQL(GPLv2).

1 Like

after separate, each integration test is a sql file?
then how to do the check like below?

result.Check(testkit.Rows("3", "2", "2"))

i have no idea how mysql do integration test so had a quick view of mysql_test,
like https://github.com/mysql/mysql-server/blob/8.0/mysql-test/t/alter_sync.test

--source include/count_sessions.inc

.test file source .inc file, .inc file has logic like if and can source other .inc,
seems complicated.

sorry, understand, not sql file, similiar like mysql.
input in .test file, expectation in .result file.
but how to check .result, are are tools?

Here are two things proposed.

  1. Factor out integration tests from unit tests.
  2. Migrate integration tests to mysql-test framework.

I’d prefer we do it in two phases since either or them benefits.

Firstly, we factor out integration tests into separated directories. Now we can handle them dedicatedly and CI automation can identify tests in types.

Secondly, we investigate whether and which framework we should move to and do some experimental.

Yes, the .test files will be used as input to the mysql-tester tool, which will compare with the .result file. If the run gives different result it will break and return the error/difference/expected outcome.
One great benefit is the possibility to record a new .result file, so you don’t need to edit all .go files when you develop a change that will impact output or legacy behaviour. You just record new .result files and review them.

I don’t think the mysql-tester tool is fully compatible with mysql-test-run perl framework (like supporting perl code in the test), but that should cover most integration tests, and also make it easy to run the integration test on a MySQL instance instead of TiDB to compare compatibility.

get it, thanks for detailed reply.
is https://github.com/pingcap/mysql-tester ready?

One more thing about moving integration tests to mysql-tester framework is that we can hardly do it entirely and highly possibly end up in an intermediate situation - we have too much tests rely on failpoints or white-box changes.

If we decide to do that, in mysql-tester syntax, it is likely we make a DSL which make things more complex and uncontrollable.