diff --git a/.gitignore b/.gitignore
index 99c59cec93c9d4f54f294ced2cc252c8cd34e9e5..fd915c1b9b0cecb7f4a3fc069e70ad8999ff4a10 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,8 +33,13 @@ target/
 cluster.id
 pids/
 modules/
-nebula-python/
+
+# tests
 !tests/Makefile
+reformat-gherkin
+nebula-python
+.pytest
+.pytest_cache
 
 # IDE
 .idea/
diff --git a/tests/.gitignore b/tests/.gitignore
deleted file mode 100644
index 4097c3de5ebe4e07db83b6d8abb1de0ce3ab9f2d..0000000000000000000000000000000000000000
--- a/tests/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-.pytest
-.pytest_cache
-reformat-gherkin
-nebula-python
diff --git a/tests/tck/utils/comparator.py b/tests/common/comparator.py
similarity index 66%
rename from tests/tck/utils/comparator.py
rename to tests/common/comparator.py
index 53ac69e7b4b23344b8b2e2fa5738b03aee85deec..691b5c5948389bf6badd9b426c2710f6769135df 100644
--- a/tests/tck/utils/comparator.py
+++ b/tests/common/comparator.py
@@ -4,33 +4,36 @@
 # attached with Common Clause Condition 1.0, found in the LICENSES directory.
 
 import math
+from functools import reduce
 
 from nebula2.data.DataObject import (
+    DataSetWrapper,
     Node,
+    PathWrapper,
     Record,
     Relationship,
-    PathWrapper,
-    DataSetWrapper,
     ValueWrapper,
 )
 
 
 class DataSetWrapperComparator:
-    def __init__(self, strict=True, order=False):
+    def __init__(self, strict=True, order=False, included=False):
         self._strict = strict
         self._order = order
+        self._included = included
 
-    def __call__(self, lhs: DataSetWrapper, rhs: DataSetWrapper):
-        return self.compare(lhs, rhs)
+    def __call__(self, resp: DataSetWrapper, expect: DataSetWrapper):
+        return self.compare(resp, expect)
 
-    def compare(self, lhs: DataSetWrapper, rhs: DataSetWrapper):
-        if lhs.get_row_size() != rhs.get_row_size():
+    def compare(self, resp: DataSetWrapper, expect: DataSetWrapper):
+        if resp.get_row_size() < expect.get_row_size():
             return False
-        if not lhs.get_col_names() == rhs.get_col_names():
+        if not resp.get_col_names() == expect.get_col_names():
             return False
         if self._order:
-            return all(self.compare_row(l, r) for (l, r) in zip(lhs, rhs))
-        return self._compare_list(lhs, rhs, self.compare_row)
+            return all(self.compare_row(l, r) for (l, r) in zip(resp, expect))
+        return self._compare_list(resp, expect, self.compare_row,
+                                  self._included)
 
     def compare_value(self, lhs: ValueWrapper, rhs: ValueWrapper):
         """
@@ -90,21 +93,40 @@ class DataSetWrapperComparator:
             for (l, r) in zip(lhs, rhs))
 
     def compare_edge(self, lhs: Relationship, rhs: Relationship):
-        if not lhs == rhs:
-            return False
-        if not self._strict:
-            return True
+        if self._strict:
+            if not lhs == rhs:
+                return False
+        else:
+            redge = rhs._value
+            if redge.src is not None and redge.dst is not None:
+                if lhs.start_vertex_id() != rhs.start_vertex_id():
+                    return False
+                if lhs.end_vertex_id() != rhs.end_vertex_id():
+                    return False
+            if redge.ranking is not None:
+                if lhs.ranking() != rhs.ranking():
+                    return False
+            if redge.name is not None:
+                if lhs.edge_name() != rhs.edge_name():
+                    return False
+            # FIXME(yee): diff None and {} test cases
+            if len(rhs.propertys()) == 0:
+                return True
         return self.compare_map(lhs.propertys(), rhs.propertys())
 
     def compare_node(self, lhs: Node, rhs: Node):
-        if lhs.get_id() != rhs.get_id():
-            return False
-        if not self._strict:
-            return True
-        if len(lhs.tags()) != len(rhs.tags()):
-            return False
-        for tag in lhs.tags():
-            if not rhs.has_tag(tag):
+        if self._strict:
+            if lhs.get_id() != rhs.get_id():
+                return False
+            if len(lhs.tags()) != len(rhs.tags()):
+                return False
+        else:
+            if rhs._value.vid is not None and lhs.get_id() != rhs.get_id():
+                return False
+            if len(lhs.tags()) < len(rhs.tags()):
+                return False
+        for tag in rhs.tags():
+            if not lhs.has_tag(tag):
                 return False
             lprops = lhs.propertys(tag)
             rprops = rhs.propertys(tag)
@@ -124,29 +146,28 @@ class DataSetWrapperComparator:
         return True
 
     def compare_list(self, lhs, rhs):
-        if len(lhs) != len(rhs):
-            return False
-        if self._strict:
-            return all(self.compare_value(l, r) for (l, r) in zip(lhs, rhs))
-        return self._compare_list(lhs, rhs, self.compare_value)
+        return len(lhs) == len(rhs) and \
+            self._compare_list(lhs, rhs, self.compare_value)
 
     def compare_row(self, lrecord: Record, rrecord: Record):
         if not lrecord.size() == rrecord.size():
             return False
-        return all(self.compare_value(l, r)
-                   for (l, r) in zip(lrecord, rrecord))
+        return all(
+            self.compare_value(l, r) for (l, r) in zip(lrecord, rrecord))
 
-    def _compare_list(self, lhs, rhs, cmp_fn):
+    def _compare_list(self, lhs, rhs, cmp_fn, included=False):
         visited = []
-        size = 0
-        for lr in lhs:
-            size += 1
+        for rr in rhs:
             found = False
-            for i, rr in enumerate(rhs):
+            for i, lr in enumerate(lhs):
                 if i not in visited and cmp_fn(lr, rr):
                     visited.append(i)
                     found = True
                     break
             if not found:
                 return False
+        lst = [1 for i in lhs]
+        size = reduce(lambda x, y: x + y, lst) if len(lst) > 0 else 0
+        if included:
+            return len(visited) <= size
         return len(visited) == size
diff --git a/tests/common/configs.py b/tests/common/configs.py
index 9dd786e9eae8f5b3324b198bba9175082d62b335..3307d5a9c66282e1b8c0db2d5d1f690a8d68f2cc 100644
--- a/tests/common/configs.py
+++ b/tests/common/configs.py
@@ -5,6 +5,14 @@
 # This source code is licensed under Apache 2.0 License,
 # attached with Common Clause Condition 1.0, found in the LICENSES directory.
 
+import os
+
+DATA_DIR = os.path.join(
+    os.path.dirname(os.path.abspath(__file__)),
+    "..",
+    "data",
+)
+
 all_configs = {'--address'        : ['address', '', 'Address of the Nebula'],
                '--user'           : ['user', 'root', 'The user of Nebula'],
                '--password'       : ['password', 'nebula', 'The password of Nebula'],
diff --git a/tests/common/nebula_service.py b/tests/common/nebula_service.py
index 06e799510f3b67428260933d9c05a6262349de65..5ebd671d4b1e5af40690749188f422205fcd0d78 100644
--- a/tests/common/nebula_service.py
+++ b/tests/common/nebula_service.py
@@ -173,16 +173,20 @@ class NebulaService(object):
         # wait nebula start
         start_time = time.time()
         if not self._check_servers_status(server_ports):
-            raise Exception(
-                f'nebula servers not ready in {time.time() - start_time}s')
-        print(f'nebula servers start ready in {time.time() - start_time}s')
+            self._collect_pids()
+            self.kill_all(signal.SIGKILL)
+            elapse = time.time() - start_time
+            raise Exception(f'nebula servers not ready in {elapse}s')
 
+        self._collect_pids()
+
+        return graph_port
+
+    def _collect_pids(self):
         for pf in glob.glob(self.work_dir + '/pids/*.pid'):
             with open(pf) as f:
                 self.pids[f.name] = int(f.readline())
 
-        return graph_port
-
     def stop(self):
         print("try to stop nebula services...")
         self.kill_all(signal.SIGTERM)
diff --git a/tests/common/types.py b/tests/common/types.py
index 1ea00e785ce0c4666583303f7c39f7efd2345d51..f18261522afc5f53c16ffe5c67995a214f0fb69b 100644
--- a/tests/common/types.py
+++ b/tests/common/types.py
@@ -4,6 +4,28 @@
 # attached with Common Clause Condition 1.0, found in the LICENSES directory.
 
 
+class SpaceDesc:
+    def __init__(self,
+                 name: str,
+                 vid_type: str = "FIXED_STRING(32)",
+                 partition_num: int = 7,
+                 replica_factor: int = 1):
+        self.name = name
+        self.vid_type = vid_type
+        self.partition_num = partition_num
+        self.replica_factor = replica_factor
+
+    def create_stmt(self) -> str:
+        return "CREATE SPACE IF NOT EXISTS `{}`(partition_num={}, replica_factor={}, vid_type={});".format(
+            self.name, self.partition_num, self.replica_factor, self.vid_type)
+
+    def use_stmt(self) -> str:
+        return f"USE `{self.name}`;"
+
+    def drop_stmt(self) -> str:
+        return f"DROP SPACE IF EXISTS `{self.name}`;"
+
+
 class Column:
     def __init__(self, index: int):
         if index < 0:
diff --git a/tests/common/utils.py b/tests/common/utils.py
index 664ab5e9218a6dd45a6cc585f405878965c762a1..cb330215a995de7e322dc177fd16fa882ac01951 100644
--- a/tests/common/utils.py
+++ b/tests/common/utils.py
@@ -5,11 +5,18 @@
 # This source code is licensed under Apache 2.0 License,
 # attached with Common Clause Condition 1.0, found in the LICENSES directory.
 
-import pdb
+import string
+import random
+import time
+import os
 
 from typing import Pattern
+from pathlib import Path
+from nebula2.gclient.net import Session
 from nebula2.common import ttypes as CommonTtypes
 from tests.common.path_value import PathVal
+from tests.common.csv_import import CSVImporter
+from tests.common.types import SpaceDesc
 
 
 def utf8b(s: str):
@@ -34,7 +41,6 @@ def _compare_values_by_pattern(real, expect):
 
 def _compare_list(rvalues, evalues):
     if len(rvalues) != len(evalues):
-        pdb.set_trace()
         return False
 
     for rval in rvalues:
@@ -44,7 +50,6 @@ def _compare_list(rvalues, evalues):
                 found = True
                 break
         if not found:
-            pdb.set_trace()
             return False
     return True
 
@@ -53,11 +58,9 @@ def _compare_map(rvalues, evalues):
     for key in rvalues:
         ev = evalues.get(key)
         if ev is None:
-            pdb.set_trace()
             return False
         rv = rvalues.get(key)
         if not compare_value(rv, ev):
-            pdb.set_trace()
             return False
     return True
 
@@ -249,7 +252,8 @@ def path_to_string(path):
 
 
 def dataset_to_string(dataset):
-    column_names = ','.join(map(lambda x: x.decode('utf-8'), dataset.column_names))
+    column_names = ','.join(
+        map(lambda x: x.decode('utf-8'), dataset.column_names))
     rows = '\n'.join(map(row_to_string, dataset.rows))
     return '\n'.join([column_names, rows])
 
@@ -310,3 +314,44 @@ def find_in_rows(row, rows):
         if found:
             return True
     return False
+
+
+def space_generator(size=6, chars=string.ascii_uppercase + string.digits):
+    return ''.join(random.choice(chars) for _ in range(size))
+
+
+def create_space(space_desc: SpaceDesc, sess: Session):
+    def exec(stmt):
+        resp = sess.execute(stmt)
+        assert resp.is_succeeded(), f"Fail to exec: {stmt}, {resp.error_msg()}"
+
+    exec(space_desc.drop_stmt())
+    exec(space_desc.create_stmt())
+    time.sleep(3)
+    exec(space_desc.use_stmt())
+
+
+def load_csv_data(pytestconfig, sess: Session, data_dir: str):
+    """
+    Before loading CSV data files, you must create and select a graph
+    space. The `schema.ngql' file only create schema about tags and
+    edges, not include space.
+    """
+    schema_path = os.path.join(data_dir, 'schema.ngql')
+
+    with open(schema_path, 'r') as f:
+        stmts = []
+        for line in f.readlines():
+            ln = line.strip()
+            if ln.startswith('--'):
+                continue
+            stmts.append(ln)
+        rs = sess.execute(' '.join(stmts))
+        assert rs.is_succeeded()
+
+    time.sleep(3)
+
+    for path in Path(data_dir).rglob('*.csv'):
+        for stmt in CSVImporter(path):
+            rs = sess.execute(stmt)
+            assert rs.is_succeeded()
diff --git a/tests/conftest.py b/tests/conftest.py
index 18795fddf8717bd0e6049dccdba065ac4728a035..ffa7e14d079e88090b9082a89f64eb04f9fd08d3 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -5,20 +5,20 @@
 # This source code is licensed under Apache 2.0 License,
 # attached with Common Clause Condition 1.0, found in the LICENSES directory.
 
-import pytest
+import json
+import logging
 import os
 import time
-import logging
-import json
 
+import pytest
 from filelock import FileLock
-from pathlib import Path
-from nebula2.gclient.net import ConnectionPool
 from nebula2.Config import Config
+from nebula2.gclient.net import ConnectionPool
 
 from tests.common.configs import all_configs
 from tests.common.nebula_service import NebulaService
-from tests.common.csv_import import CSVImporter
+from tests.common.types import SpaceDesc
+from tests.common.utils import create_space, load_csv_data
 
 tests_collected = set()
 tests_executed = set()
@@ -26,6 +26,7 @@ data_dir = os.getenv('NEBULA_DATA_DIR')
 
 CURR_PATH = os.path.dirname(os.path.abspath(__file__))
 
+
 # pytest hook to handle test collection when xdist is used (parallel tests)
 # https://github.com/pytest-dev/pytest-xdist/pull/35/commits (No official documentation available)
 def pytest_xdist_node_collection_finished(node, ids):
@@ -113,7 +114,8 @@ def conn_pool(pytestconfig, worker_id, tmp_path_factory):
             data["num_workers"] += 1
             fn.write_text(json.dumps(data))
         else:
-            nb = NebulaService(build_dir, project_dir, rm_dir.lower() == "true")
+            nb = NebulaService(build_dir, project_dir,
+                               rm_dir.lower() == "true")
             nb.install()
             port = nb.start()
             pool = get_conn_pool("localhost", port)
@@ -141,7 +143,7 @@ def conn_pool(pytestconfig, worker_id, tmp_path_factory):
         os.remove(str(fn))
 
 
-@pytest.fixture(scope="session")
+@pytest.fixture(scope="class")
 def session(conn_pool, pytestconfig):
     user = pytestconfig.getoption("user")
     password = pytestconfig.getoption("password")
@@ -150,70 +152,43 @@ def session(conn_pool, pytestconfig):
     sess.release()
 
 
-def load_csv_data(pytestconfig, conn_pool, folder: str):
-    data_dir = os.path.join(CURR_PATH, 'data', folder)
-    schema_path = os.path.join(data_dir, 'schema.ngql')
-
-    user = pytestconfig.getoption("user")
-    password = pytestconfig.getoption("password")
-    sess = conn_pool.get_session(user, password)
-
-    with open(schema_path, 'r') as f:
-        stmts = []
-        for line in f.readlines():
-            ln = line.strip()
-            if ln.startswith('--'):
-                continue
-            stmts.append(ln)
-        rs = sess.execute(' '.join(stmts))
-        assert rs.is_succeeded()
-
-    time.sleep(3)
-
-    for path in Path(data_dir).rglob('*.csv'):
-        for stmt in CSVImporter(path):
-            rs = sess.execute(stmt)
-            assert rs.is_succeeded()
-
-    sess.release()
+def load_csv_data_once(tmp_path_factory, pytestconfig, worker_id, conn_pool,
+                       space_desc: SpaceDesc):
+    space_name = space_desc.name
+    root_tmp_dir = tmp_path_factory.getbasetemp().parent
+    fn = root_tmp_dir / f"csv-data-{space_name}"
+    with FileLock(str(fn) + ".lock"):
+        if fn.is_file():
+            logging.info(
+                f"session-{worker_id} need not to load {space_name} csv data")
+            yield space_desc
+            return
+        data_dir = os.path.join(CURR_PATH, 'data', space_name)
+        user = pytestconfig.getoption("user")
+        password = pytestconfig.getoption("password")
+        sess = conn_pool.get_session(user, password)
+        create_space(space_desc, sess)
+        load_csv_data(pytestconfig, sess, data_dir)
+        sess.release()
+        fn.write_text(space_name)
+        logging.info(f"session-{worker_id} load {space_name} csv data")
+        yield space_desc
+        os.remove(str(fn))
 
 
 # TODO(yee): optimize data load fixtures
 @pytest.fixture(scope="session")
 def load_nba_data(conn_pool, pytestconfig, tmp_path_factory, worker_id):
-    root_tmp_dir = tmp_path_factory.getbasetemp().parent
-    fn = root_tmp_dir / "csv-data-nba"
-    load = False
-    with FileLock(str(fn) + ".lock"):
-        if not fn.is_file():
-            load_csv_data(pytestconfig, conn_pool, "nba")
-            fn.write_text("nba")
-            logging.info(f"session-{worker_id} load nba csv data")
-            load = True
-        else:
-            logging.info(f"session-{worker_id} need not to load nba csv data")
-    yield
-    if load:
-        os.remove(str(fn))
+    space_desc = SpaceDesc(name="nba", vid_type="FIXED_STRING(30)")
+    yield from load_csv_data_once(tmp_path_factory, pytestconfig, worker_id,
+                                  conn_pool, space_desc)
 
 
 @pytest.fixture(scope="session")
 def load_student_data(conn_pool, pytestconfig, tmp_path_factory, worker_id):
-    root_tmp_dir = tmp_path_factory.getbasetemp().parent
-    fn = root_tmp_dir / "csv-data-student"
-    load = False
-    with FileLock(str(fn) + ".lock"):
-        if not fn.is_file():
-            load_csv_data(pytestconfig, conn_pool, "student")
-            fn.write_text("student")
-            logging.info(f"session-{worker_id} load student csv data")
-            load = True
-        else:
-            logging.info(
-                f"session-{worker_id} need not to load student csv data")
-    yield
-    if load:
-        os.remove(str(fn))
+    space_desc = SpaceDesc(name="student", vid_type="FIXED_STRING(8)")
+    yield from load_csv_data_once(tmp_path_factory, pytestconfig, worker_id,
+                                  conn_pool, space_desc)
 
 
 # TODO(yee): Delete this when we migrate all test cases
diff --git a/tests/data/nba/schema.ngql b/tests/data/nba/schema.ngql
index 7da4463bb16f4880bd7408d974d4b9aa5b2fbd25..7ac3137e8c1512102299e4f8b9cc002419c7e48a 100644
--- a/tests/data/nba/schema.ngql
+++ b/tests/data/nba/schema.ngql
@@ -1,6 +1,3 @@
-DROP SPACE IF EXISTS nba;
-CREATE SPACE nba(partition_num=7, replica_factor=1, vid_type=FIXED_STRING(30));
-USE nba;
 CREATE TAG IF NOT EXISTS player(name string, age int);
 CREATE TAG IF NOT EXISTS team(name string);
 CREATE TAG IF NOT EXISTS bachelor(name string, speciality string);
diff --git a/tests/data/student/schema.ngql b/tests/data/student/schema.ngql
index 058dc5feb186c39238f9a68216c44eea98bcd58f..d019931e3b40b8b34c1663d3364befc7f7b91a22 100644
--- a/tests/data/student/schema.ngql
+++ b/tests/data/student/schema.ngql
@@ -1,6 +1,3 @@
-DROP SPACE IF EXISTS student;
-CREATE SPACE IF NOT EXISTS student(partition_num=7, replica_factor=1, vid_type=FIXED_STRING(8));
-USE student;
 CREATE TAG IF NOT EXISTS person(name string, age int, gender string);
 CREATE TAG IF NOT EXISTS teacher(grade int, subject string);
 CREATE TAG IF NOT EXISTS student(grade int, hobby string DEFAULT "");
diff --git a/tests/tck/conftest.py b/tests/tck/conftest.py
index 2bee9e09030bc5432abd160a73074469711f4b7d..11441d20740677847ad09bef3c03b8e667631ece 100644
--- a/tests/tck/conftest.py
+++ b/tests/tck/conftest.py
@@ -4,52 +4,137 @@
 # attached with Common Clause Condition 1.0, found in the LICENSES directory.
 
 import functools
+import os
+import time
+import pytest
+import io
+import csv
 
-from pytest_bdd import given, when, then, parsers
 from nebula2.data.DataObject import DataSetWrapper
-from tests.tck.utils.table import table, dataset
-from tests.tck.utils.comparator import DataSetWrapperComparator
+from nebula2.graph.ttypes import ErrorCode
+from pytest_bdd import given, parsers, then, when
+
+from tests.common.comparator import DataSetWrapperComparator
+from tests.common.configs import DATA_DIR
+from tests.common.types import SpaceDesc
+from tests.common.utils import create_space, load_csv_data, space_generator
+from tests.tck.utils.table import dataset, table
 
 parse = functools.partial(parsers.parse)
 
 
-@given(
-    parse('a graph with space named "nba"'),
-    target_fixture="nba_space",
-)
-def nba_space(load_nba_data, session):
-    rs = session.execute('USE nba;')
-    assert rs.is_succeeded()
-    return {"result_set": None}
+@pytest.fixture
+def graph_spaces():
+    return dict(result_set=None)
+
+
+@given(parse('a graph with space named "{space}"'))
+def preload_space(space, load_nba_data, load_student_data, session,
+                  graph_spaces):
+    if space == "nba":
+        graph_spaces["space_desc"] = load_nba_data
+    elif space == "student":
+        graph_spaces["space_desc"] = load_student_data
+    else:
+        raise ValueError(f"Invalid space name given: {space}")
+    rs = session.execute(f'USE {space};')
+    assert rs.is_succeeded(), f"Fail to use space `{space}': {rs.error_msg()}"
+
+
+@given("an empty graph")
+def empty_graph(session, graph_spaces):
+    pass
+
+
+@given(parse("having executed:\n{query}"))
+def having_executed(query, session):
+    ngql = " ".join(query.splitlines())
+    resp = session.execute(ngql)
+    assert resp.is_succeeded(), \
+        f"Fail to execute {ngql}, error: {resp.error_msg()}"
+
+
+@given(parse("create a space with following options:\n{options}"))
+def new_space(options, session, graph_spaces):
+    lines = csv.reader(io.StringIO(options), delimiter="|")
+    opts = {line[1]: line[2] for line in lines}
+    name = "EmptyGraph_" + space_generator()
+    space_desc = SpaceDesc(
+        name=name,
+        partition_num=int(opts.get("partition_num", 7)),
+        replica_factor=int(opts.get("replica_factor", 1)),
+        vid_type=opts.get("vid_type", "FIXED_STRING(30)"),
+    )
+    create_space(space_desc, session)
+    graph_spaces["space_desc"] = space_desc
+    graph_spaces["drop_space"] = True
+
+
+@given(parse('import "{data}" csv data'))
+def import_csv_data(data, graph_spaces, session, pytestconfig):
+    data_dir = os.path.join(DATA_DIR, data)
+    space_desc = graph_spaces["space_desc"]
+    assert space_desc is not None
+    resp = session.execute(space_desc.use_stmt())
+    assert resp.is_succeeded(), \
+        f"Fail to use {space_desc.name}, {resp.error_msg()}"
+    load_csv_data(pytestconfig, session, data_dir)
 
 
 @when(parse("executing query:\n{query}"))
-def executing_query(query, nba_space, session):
+def executing_query(query, graph_spaces, session):
     ngql = " ".join(query.splitlines())
-    nba_space['result_set'] = session.execute(ngql)
+    graph_spaces['result_set'] = session.execute(ngql)
 
 
-@then(parse("the result should be, in any order:\n{result}"))
-def result_should_be(result, nba_space):
-    rs = nba_space['result_set']
-    assert rs.is_succeeded()
+@given(parse("wait {secs:d} seconds"))
+@when(parse("wait {secs:d} seconds"))
+def wait(secs):
+    time.sleep(secs)
+
+
+def cmp_dataset(graph_spaces,
+                result,
+                order: bool,
+                strict: bool,
+                included=False) -> None:
+    rs = graph_spaces['result_set']
+    assert rs.is_succeeded(), f"Response failed: {rs.error_msg()}"
     ds = DataSetWrapper(dataset(table(result)))
-    dscmp = DataSetWrapperComparator(strict=True, order=False)
-    assert dscmp(rs._data_set_wrapper, ds)
+    dscmp = DataSetWrapperComparator(strict=strict,
+                                     order=order,
+                                     included=included)
+    assert dscmp(rs._data_set_wrapper, ds), \
+        f"Response: {str(rs._data_set_wrapper)} vs. Expected: {str(ds)}"
+
+
+@then(parse("the result should be, in order:\n{result}"))
+def result_should_be_in_order(result, graph_spaces):
+    cmp_dataset(graph_spaces, result, order=True, strict=True)
+
+
+@then(
+    parse("the result should be, in order, with relax comparision:\n{result}"))
+def result_should_be_in_order_relax_cmp(result, graph_spaces):
+    cmp_dataset(graph_spaces, result, order=True, strict=False)
+
+
+@then(parse("the result should be, in any order:\n{result}"))
+def result_should_be(result, graph_spaces):
+    cmp_dataset(graph_spaces, result, order=False, strict=True)
 
 
 @then(
     parse(
         "the result should be, in any order, with relax comparision:\n{result}"
     ))
-def result_should_be_relax_cmp(result, nba_space):
-    rs = nba_space['result_set']
-    assert rs.is_succeeded()
-    ds = dataset(table(result))
-    dsw = DataSetWrapper(ds)
-    dscmp = DataSetWrapperComparator(strict=False, order=False)
-    assert dscmp(rs._data_set_wrapper, dsw), \
-        f"Response: {str(rs)} vs. Expected: {str(dsw)}"
+def result_should_be_relax_cmp(result, graph_spaces):
+    cmp_dataset(graph_spaces, result, order=False, strict=False)
+
+
+@then(parse("the result should include:\n{result}"))
+def result_should_include(result, graph_spaces):
+    cmp_dataset(graph_spaces, result, order=False, strict=True, included=True)
 
 
 @then("no side effects")
@@ -57,6 +142,31 @@ def no_side_effects():
     pass
 
 
-@then(parse("a TypeError should be raised at runtime: InvalidArgumentValue"))
-def raised_type_error():
-    pass
+@then("the execution should be successful")
+def execution_should_be_succ(graph_spaces):
+    rs = graph_spaces["result_set"]
+    assert rs is not None, "Please execute a query at first"
+    assert rs.is_succeeded(), f"Response failed: {rs.error_msg()}"
+
+
+@then(parse("a {err_type} should be raised at {time}:{msg}"))
+def raised_type_error(err_type, time, msg, graph_spaces):
+    res = graph_spaces["result_set"]
+    assert not res.is_succeeded(), "Response should be failed"
+    if res.error_code() == ErrorCode.E_EXECUTION_ERROR:
+        expect_msg = f"{msg.strip()}"
+        assert err_type.strip() == "ExecutionError"
+    else:
+        expect_msg = f"{err_type.strip()}: {msg.strip()}"
+    assert res.error_msg() == expect_msg, \
+        "Response error msg: {res.error_msg()} vs. Expected: {expect_msg}"
+
+
+@then("drop the used space")
+def drop_used_space(session, graph_spaces):
+    drop_space = graph_spaces.get("drop_space", False)
+    if not drop_space:
+        return
+    space_desc = graph_spaces["space_desc"]
+    resp = session.execute(space_desc.drop_stmt())
+    assert resp.is_succeeded(), f"Fail to drop space {space_desc.name}"
diff --git a/tests/tck/features/expression/Yield.feature b/tests/tck/features/expression/Yield.feature
new file mode 100644
index 0000000000000000000000000000000000000000..9e3b6cc3972338dc6d0b1d42b72a97d2ad86e674
--- /dev/null
+++ b/tests/tck/features/expression/Yield.feature
@@ -0,0 +1,12 @@
+Feature: Yield
+
+  @skip
+  Scenario: yield without chosen space
+    Given an empty graph
+    When executing query:
+      """
+      YIELD 1+1 AS sum
+      """
+    Then the result should be, in any order:
+      | sum |
+      | 2   |
diff --git a/tests/tck/features/job/job.feature b/tests/tck/features/job/job.feature
deleted file mode 100644
index a100b4143c3c162df42d10d3fa82ed67daefe03e..0000000000000000000000000000000000000000
--- a/tests/tck/features/job/job.feature
+++ /dev/null
@@ -1,25 +0,0 @@
-Feature: Job
-
-  @skip
-  Scenario: submit snapshot job
-    Given a graph with data "nba" named "nba_snapshot"
-    When submitting job:
-      """
-      SUBMIT JOB SNAPSHOT
-      """
-    Then the result should be, in any order, with "10" seconds timout:
-      | Job Id | Command  | Status   |
-      | r"\d+" | SNAPSHOT | FINISHED |
-    And no side effects
-    When executing query:
-      """
-      MATCH (v:player {age: 29})
-      RETURN v.name AS Name
-      """
-    Then the result should be, in any order:
-      | Name               |
-      | 'James Harden'     |
-      | 'Jonathon Simmons' |
-      | 'Klay Thompson'    |
-      | 'Dejounte Murray'  |
-    And no side effects
diff --git a/tests/tck/features/job/snapshot.feature b/tests/tck/features/job/snapshot.feature
deleted file mode 100644
index 9b9b0ecfa7fd5c37fdeac828b4132f98e7b49347..0000000000000000000000000000000000000000
--- a/tests/tck/features/job/snapshot.feature
+++ /dev/null
@@ -1,25 +0,0 @@
-Feature: Snapshot
-
-  @skip
-  Scenario: submit snapshot job
-    Given a graph with data "nba" named "nba_snapshot"
-    When submitting job:
-      """
-      SUBMIT JOB SNAPSHOT
-      """
-    Then the result should be, in any order, with "10" seconds timeout:
-      | Job Id | Command  | Status   |
-      | r"\d+" | SNAPSHOT | FINISHED |
-    And no side effects
-    When executing query:
-      """
-      MATCH (v:player {age: 29})
-      RETURN v.name AS Name
-      """
-    Then the result should be, in any order:
-      | Name               |
-      | 'James Harden'     |
-      | 'Jonathon Simmons' |
-      | 'Klay Thompson'    |
-      | 'Dejounte Murray'  |
-    And no side effects
diff --git a/tests/tck/features/match/Base.feature b/tests/tck/features/match/Base.feature
index dc4be60fb4d022884a4a8b8504bd2b29ee22b45b..66a3bc7c6d224dca8575f70b767bf8a6c9871d30 100644
--- a/tests/tck/features/match/Base.feature
+++ b/tests/tck/features/match/Base.feature
@@ -20,7 +20,6 @@ Feature: Basic match
       | 'Ray Allen'     | 43  |
       | 'David West'    | 38  |
       | 'Tracy McGrady' | 39  |
-    And no side effects
     When executing query:
       """
       MATCH (v:player {age: 29})
@@ -32,4 +31,3 @@ Feature: Basic match
       | 'Jonathon Simmons' |
       | 'Klay Thompson'    |
       | 'Dejounte Murray'  |
-    And no side effects
diff --git a/tests/tck/features/match/VariableLengthRelationship.feature b/tests/tck/features/match/VariableLengthPattern.feature
similarity index 99%
rename from tests/tck/features/match/VariableLengthRelationship.feature
rename to tests/tck/features/match/VariableLengthPattern.feature
index 4e6d3b9146a950c52a9a72c8d9c28af773362a49..c715ceb582e5680ccdbac543164d5dbec964c4b9 100644
--- a/tests/tck/features/match/VariableLengthRelationship.feature
+++ b/tests/tck/features/match/VariableLengthPattern.feature
@@ -1,4 +1,4 @@
-Feature: Variable length relationship match (m to n)
+Feature: Variable length Pattern match (m to n)
 
   Background:
     Given a graph with space named "nba"
diff --git a/tests/tck/features/parser/Space.feature b/tests/tck/features/parser/Space.feature
new file mode 100644
index 0000000000000000000000000000000000000000..42af115c6dd7aeb365b0d6fa881859d7b9eddf7a
--- /dev/null
+++ b/tests/tck/features/parser/Space.feature
@@ -0,0 +1,41 @@
+Feature: Test space steps
+
+  Scenario: Test space steps
+    Given an empty graph
+    And create a space with following options:
+      | partition_num  | 9                |
+      | replica_factor | 1                |
+      | vid_type       | FIXED_STRING(30) |
+    And import "nba" csv data
+    And having executed:
+      """
+      CREATE TAG IF NOT EXISTS `test_tag`(name string)
+      """
+    And wait 3 seconds
+    When executing query:
+      """
+      MATCH (v:player{name: "Tim Duncan"})
+      RETURN v.name AS Name
+      """
+    Then the result should be, in any order:
+      | Name         |
+      | "Tim Duncan" |
+    When executing query:
+      """
+      MATCH (v:player{name: "Tim Duncan"})
+      RETURN v.name AS Name
+      """
+    Then the result should include:
+      | Name         |
+      | "Tim Duncan" |
+    When executing query:
+      """
+      SHOW HOSTS
+      """
+    Then the execution should be successful
+    When executing query:
+      """
+      YIELD $-.id AS id
+      """
+    Then a SemanticError should be raised at runtime: `$-.id', not exist prop `id'
+    Then drop the used space
diff --git a/tests/tck/utils/table.py b/tests/tck/utils/table.py
index 2c5f66c3bf3cc6f58ee7371de51ea77ba7c71ee1..723ba6f188f0851dd6698bf36bdfa1b197e437ca 100644
--- a/tests/tck/utils/table.py
+++ b/tests/tck/utils/table.py
@@ -3,26 +3,27 @@
 # This source code is licensed under Apache 2.0 License,
 # attached with Common Clause Condition 1.0, found in the LICENSES directory.
 
+import csv
+import io
+
 from tests.tck.utils.nbv import parse
-from nebula2.common.ttypes import DataSet, Row
+from nebula2.common.ttypes import DataSet, Row, Value
+
+
+def _parse_value(cell: str) -> Value:
+    value = parse(cell)
+    assert value is not None, f'parse error: column is {cell}'
+    return value
 
 
 def table(text):
-    lines = text.splitlines()
-    assert len(lines) >= 1
-
-    def parse_line(line):
-        return list(
-            map(lambda x: x.strip(), filter(lambda x: x, line.split('|'))))
-
-    table = []
-    column_names = list(map(lambda x: bytes(x, 'utf-8'), parse_line(lines[0])))
-    for line in lines[1:]:
-        row = {}
-        cells = parse_line(line)
-        for i, cell in enumerate(cells):
-            row[column_names[i]] = cell
-        table.append(row)
+    lines = list(csv.reader(io.StringIO(text), delimiter="|"))
+    header = lines[0][1:-1]
+    column_names = list(map(lambda x: bytes(x.strip(), 'utf8'), header))
+    table = [{
+        column_name: cell.strip()
+        for (column_name, cell) in zip(column_names, line[1:-1])
+    } for line in lines[1:]]
 
     return {
         "column_names": column_names,
@@ -33,14 +34,8 @@ def table(text):
 def dataset(string_table):
     ds = DataSet()
     ds.column_names = string_table['column_names']
-    ds.rows = []
-    for row in string_table['rows']:
-        nrow = Row()
-        nrow.values = []
-        for column in ds.column_names:
-            value = parse(row[column])
-            assert value is not None, \
-                    f'parse error: column is {column}:{row[column]}'
-            nrow.values.append(value)
-        ds.rows.append(nrow)
+    ds.rows = [
+        Row(values=[_parse_value(row[column]) for column in ds.column_names])
+        for row in string_table['rows']
+    ]
     return ds
diff --git a/tests/tck/utils/utils.py b/tests/tck/utils/utils.py
deleted file mode 100644
index fdf6309d155fe76532fc76ea58955c2aa264bdf6..0000000000000000000000000000000000000000
--- a/tests/tck/utils/utils.py
+++ /dev/null
@@ -1,66 +0,0 @@
-# Copyright (c) 2020 vesoft inc. All rights reserved.
-#
-# This source code is licensed under Apache 2.0 License,
-# attached with Common Clause Condition 1.0, found in the LICENSES directory.
-#
-
-from nebula2.common.ttypes import Value,Vertex,Edge,Path,Step,NullType,DataSet,Row,List
-import nbv
-from behave import model as bh
-
-def parse(input):
-    if isinstance(input, bh.Table):
-        return parse_table(input)
-    elif isinstance(input, bh.Row):
-        return parse_row(input)
-    else:
-        raise Exception('Unable to parse %s' % str(input))
-
-def parse_table(table):
-    names = table.headings
-    rows = []
-    for row in table:
-        rows.append(parse_row(row))
-    return DataSet(column_names = table.headings, rows = rows)
-
-def parse_row(row):
-    list = []
-    for cell in row:
-        v = nbv.parse(cell)
-        if v is None:
-            raise Exception('Unable to parse %s' % cell)
-        list.append(v)
-    return Row(list)
-
-if __name__ == '__main__':
-    headings = ['m', 'r', 'n']
-    rows = [
-        [
-            '1', '2', '3'
-        ],
-        [
-            '()', '-->', '()'
-        ],
-        [
-            '("vid")', '<-[:e "1" -> "2" @-1 {p1: 0, p2: [1, 2, 3]}]-', '()'
-        ],
-        [
-            '<()-->()<--()>', '()', '"prop"'
-        ],
-        [
-            'EMPTY', 'NULL', 'BAD_TYPE'
-        ],
-    ]
-    expected = DataSet(column_names = headings,\
-            rows = [
-                Row([Value(iVal=1), Value(iVal=2), Value(iVal=3)]),
-                Row([Value(vVal=Vertex()), Value(eVal=Edge(type=1)), Value(vVal=Vertex())]),
-                Row([Value(vVal=Vertex('vid')), Value(eVal=Edge(name='e',type=-1,src='1',dst='2',ranking=-1,props={'p1': Value(iVal=0), 'p2': Value(lVal=List([Value(iVal=1),Value(iVal=2),Value(iVal=3)]))})), Value(vVal=Vertex())]),
-                Row([Value(pVal=Path(src=Vertex(),steps=[Step(type=1,dst=Vertex()),Step(type=-1,dst=Vertex())])), Value(vVal=Vertex()), Value(sVal='prop')]),
-                Row([Value(), Value(nVal=NullType.__NULL__), Value(nVal=NullType.BAD_TYPE)])
-            ])
-
-    table = bh.Table(headings = headings, rows = rows)
-    dataset = parse(table)
-    assert dataset == expected,\
-                    "Parsed DataSet doesn't match, \nexpected: %s, \nactual: %s" % (expected, dataset)