diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
deleted file mode 100644
index f33989cadd452c87d17e0f93340381bc813b7f0b..0000000000000000000000000000000000000000
--- a/tests/CMakeLists.txt
+++ /dev/null
@@ -1,26 +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.
-
-if(NOT ${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR})
-    find_program(PYTHON_EXECUTABLE python3
-        /usr/bin /usr/local/bin
-        NO_CMAKE_ENVIRONMENT_PATH
-        NO_SYSTEM_ENVIRONMENT_PATH
-    )
-    find_program(PYTHON_EXECUTABLE python)
-
-    configure_file(
-        ${CMAKE_CURRENT_SOURCE_DIR}/ntr.out-of-source
-        ${CMAKE_CURRENT_BINARY_DIR}/nebula-test-run.py
-        @ONLY
-    )
-endif()
-
-execute_process(
-    COMMAND chmod +x nebula-test-run.py
-    COMMAND ${CMAKE_COMMAND} -E create_symlink ./nebula-test-run.py ntr
-    COMMAND ${CMAKE_COMMAND} -E create_symlink ./nebula-test-run.py nebula-test-run
-    WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
-)
diff --git a/tests/Makefile b/tests/Makefile
index fb9f95cf558ac76aff319977811af9dba25a3c09..27eff723f10f3803f871c04d84da81bc0c94a9f4 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -3,13 +3,14 @@
 # This source code is licensed under Apache 2.0 License,
 # attached with Common Clause Condition 1.0, found in the LICENSES directory.
 
-.PHONY: fmt check init clean test
+.PHONY: fmt check init init-all clean test tck
 
 PYPI_MIRROR = https://mirrors.aliyun.com/pypi/simple/
 CURR_DIR = $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
 gherkin_fmt = ~/.local/bin/reformat-gherkin
 
 RM_DIR ?= true
+TEST_DIR ?= $(CURR_DIR)
 
 install-deps:
 	pip3 install --user -U setuptools wheel -i $(PYPI_MIRROR)
@@ -38,10 +39,10 @@ check:
 	@find $(CURR_DIR)/tck/features -type f -iname "*.feature" -print | xargs $(gherkin_fmt) --check
 
 test:
-	cd $(CURR_DIR) && python3 -m pytest -n8 --dist=loadfile --rm_dir=$(RM_DIR) -m "not skip"
+	cd $(CURR_DIR) && python3 -m pytest -n8 --dist=loadfile --rm_dir=$(RM_DIR) -m "not skip" $(TEST_DIR)
 
 tck:
-	cd $(CURR_DIR) && python3 -m pytest -n8 --dist=loadfile --rm_dir=$(RM_DIR) -m "not skip" $(CURR_DIR)/tck/
+	cd $(CURR_DIR) && python3 -m pytest --gherkin-terminal-reporter --gherkin-terminal-reporter-expanded --rm_dir=$(RM_DIR) -m "not skip" $(CURR_DIR)/tck/
 
 clean:
 	@rm -rf $(CURR_DIR)/nebula-python $(CURR_DIR)/reformat-gherkin $(CURR_DIR)/.pytest $(CURR_DIR)/.pytest_cache
diff --git a/tests/common/comparator.py b/tests/common/comparator.py
index 4fdd23b49fefc7c7fe24d27560f729946d0dcc49..fcb994a26782fc9e1213912d3b7efd647a1bec98 100644
--- a/tests/common/comparator.py
+++ b/tests/common/comparator.py
@@ -26,11 +26,13 @@ class DataSetComparator:
                  strict=True,
                  order=False,
                  included=False,
-                 decode_type: str = 'utf-8'):
+                 decode_type: str = 'utf-8',
+                 vid_fn=None):
         self._strict = strict
         self._order = order
         self._included = included
         self._decode_type = decode_type
+        self._vid_fn = vid_fn
 
     def __call__(self, resp: DataSet, expect: DataSet):
         return self.compare(resp, expect)
@@ -43,20 +45,23 @@ class DataSetComparator:
 
     def compare(self, resp: DataSet, expect: DataSet):
         if all(x is None for x in [expect, resp]):
-            return True
+            return True, None
         if None in [expect, resp]:
-            return False
+            return False, -1
         if len(resp.rows) < len(expect.rows):
-            return False
+            return False, -1
         if len(resp.column_names) != len(expect.column_names):
-            return False
+            return False, -1
         for (ln, rn) in zip(resp.column_names, expect.column_names):
             if ln != self.bstr(rn):
-                return False
+                return False, -2
         if self._order:
-            return all(
-                self.compare_row(l, r)
-                for (l, r) in zip(resp.rows, expect.rows))
+            for i in range(0, len(expect.rows)):
+                if not self.compare_row(resp.rows[i], expect.rows[i]):
+                    return False, i
+            if self._included:
+                return True, None
+            return len(resp.rows) == len(expect.rows), -1
         return self._compare_list(resp.rows, expect.rows, self.compare_row,
                                   self._included)
 
@@ -130,7 +135,8 @@ class DataSetComparator:
                 return False
             lvals = lhs.get_uVal().values
             rvals = rhs.get_uVal().values
-            return self._compare_list(lvals, rvals, self.compare_value)
+            res, _ = self._compare_list(lvals, rvals, self.compare_value)
+            return res
         if lhs.getType() == Value.MVAL:
             if not rhs.getType() == Value.MVAL:
                 return False
@@ -202,14 +208,16 @@ class DataSetComparator:
             if not lhs.ranking == rhs.ranking:
                 return False
             rsrc, rdst = self.eid(rhs, lhs.type)
-            if lhs.src != rsrc or lhs.dst != rdst:
+            if not (self.compare_vid(lhs.src, rsrc)
+                    and self.compare_vid(lhs.dst, rdst)):
                 return False
             if rhs.props is None or len(lhs.props) != len(rhs.props):
                 return False
         else:
             if rhs.src is not None and rhs.dst is not None:
                 rsrc, rdst = self.eid(rhs, lhs.type)
-                if lhs.src != rsrc or lhs.dst != rdst:
+                if not (self.compare_vid(lhs.src, rsrc)
+                        and self.compare_vid(lhs.dst, rdst)):
                     return False
             if rhs.ranking is not None:
                 if lhs.ranking != rhs.ranking:
@@ -224,18 +232,33 @@ class DataSetComparator:
     def bstr(self, vid) -> bytes:
         return self.b(vid) if type(vid) == str else vid
 
+    def compare_vid(
+            self,
+            lid: Union[int, bytes],
+            rid: Union[int, bytes, str],
+    ) -> bool:
+        if type(lid) is bytes:
+            return type(rid) in [str, bytes] and lid == self.bstr(rid)
+        if type(lid) is int:
+            if type(rid) is int:
+                return lid == rid
+            if type(rid) not in [str, bytes] or self._vid_fn is None:
+                return False
+            return lid == self._vid_fn(rid)
+        return False
+
     def compare_node(self, lhs: Vertex, rhs: Vertex):
         rtags = []
         if self._strict:
             assert rhs.vid is not None
-            if not lhs.vid == self.bstr(rhs.vid):
+            if not self.compare_vid(lhs.vid, rhs.vid):
                 return False
             if rhs.tags is None or len(lhs.tags) != len(rhs.tags):
                 return False
             rtags = rhs.tags
         else:
             if rhs.vid is not None:
-                if not lhs.vid == self.bstr(rhs.vid):
+                if not self.compare_vid(lhs.vid, rhs.vid):
                     return False
             if rhs.tags is not None and len(lhs.tags) < len(rhs.tags):
                 return False
@@ -268,8 +291,10 @@ class DataSetComparator:
         return True
 
     def compare_list(self, lhs: List[Value], rhs: List[Value]):
-        return len(lhs) == len(rhs) and \
-            self._compare_list(lhs, rhs, self.compare_value)
+        if len(lhs) != len(rhs):
+            return False
+        res, _ = self._compare_list(lhs, rhs, self.compare_value)
+        return res
 
     def compare_row(self, lhs: Row, rhs: Row):
         if not len(lhs.values) == len(rhs.values):
@@ -279,7 +304,7 @@ class DataSetComparator:
 
     def _compare_list(self, lhs, rhs, cmp_fn, included=False):
         visited = []
-        for rr in rhs:
+        for j, rr in enumerate(rhs):
             found = False
             for i, lr in enumerate(lhs):
                 if i not in visited and cmp_fn(lr, rr):
@@ -287,8 +312,8 @@ class DataSetComparator:
                     found = True
                     break
             if not found:
-                return False
+                return False, j
         size = len(lhs)
         if included:
-            return len(visited) <= size
-        return len(visited) == size
+            return len(visited) <= size, -1
+        return len(visited) == size, -1
diff --git a/tests/common/dataset_printer.py b/tests/common/dataset_printer.py
index 31d06cae0528e0056e639ccace2b04ef04012b8a..b2534bcb82dcfaedc1948d3ffddf205ede25d080 100644
--- a/tests/common/dataset_printer.py
+++ b/tests/common/dataset_printer.py
@@ -27,10 +27,11 @@ class DataSetPrinter:
         return str(v)
 
     def ds_to_string(self, ds: DataSet) -> str:
-        col_names = '[' + ','.join(self.sstr(col)
-                                   for col in ds.column_names) + ']'
-        data_rows = '\n'.join('[' + self.list_to_string(row.values) + ']'
-                              for row in ds.rows)
+        col_names = '|' + '|'.join(self.sstr(col)
+                                   for col in ds.column_names) + '|'
+        data_rows = '\n'.join(
+            f'{i}: |' + self.list_to_string(row.values, delimiter='|') + '|'
+            for (i, row) in enumerate(ds.rows))
         return '\n'.join([col_names, data_rows])
 
     def to_string(self, val: Value):
@@ -74,19 +75,19 @@ class DataSetPrinter:
             return self.ds_to_string(val.get_gVal())
         return ""
 
-    def list_to_string(self, lst: List[Value]) -> str:
-        return ','.join(self.to_string(val) for val in lst)
+    def list_to_string(self, lst: List[Value], delimiter: str = ",") -> str:
+        return delimiter.join(self.to_string(val) for val in lst)
 
     def vertex_to_string(self, v: Vertex):
         if v.vid is None:
             return "()"
         if v.tags is None:
-            return f'("{self.vid(v.vid)}")'
+            return f'({self.vid(v.vid)})'
         tags = []
         for tag in v.tags:
             name = self.sstr(tag.name)
             tags.append(f":{name}{self.map_to_string(tag.props)}")
-        return f'("{self.vid(v.vid)}"{"".join(tags)})'
+        return f'({self.vid(v.vid)}{"".join(tags)})'
 
     def map_to_string(self, m: dict) -> str:
         if m is None:
@@ -97,7 +98,7 @@ class DataSetPrinter:
     def edge_to_string(self, e: Edge) -> str:
         name = "" if e.name is None else ":" + self.sstr(e.name)
         arrow = "->" if e.type is None or e.type > 0 else "<-"
-        direct = f'"{self.vid(e.src)}"{arrow}"{self.vid(e.dst)}"'
+        direct = f'{self.vid(e.src)}{arrow}{self.vid(e.dst)}'
         rank = "" if e.ranking is None else f"@{e.ranking}"
         return f"[{name} {direct}{rank}{self.map_to_string(e.props)}]"
 
diff --git a/tests/common/nebula_service.py b/tests/common/nebula_service.py
index 5ebd671d4b1e5af40690749188f422205fcd0d78..63b4fdb572bd74e62c80a94bc93024a54bbfe1fa 100644
--- a/tests/common/nebula_service.py
+++ b/tests/common/nebula_service.py
@@ -123,7 +123,7 @@ class NebulaService(object):
         for port in ports:
             ports_status[port] = False
 
-        for i in range(0, 30):
+        for i in range(0, 20):
             for port in ports_status:
                 if ports_status[port]:
                     continue
@@ -191,7 +191,7 @@ class NebulaService(object):
         print("try to stop nebula services...")
         self.kill_all(signal.SIGTERM)
 
-        max_retries = 30
+        max_retries = 20
         while self.is_procs_alive() and max_retries >= 0:
             time.sleep(1)
             max_retries = max_retries-1
diff --git a/tests/common/types.py b/tests/common/types.py
index b6292fcd0a1aab91536c8d46ed91468f77149a49..3686a12a481c6e77ba54410331f71b0a0f27745f 100644
--- a/tests/common/types.py
+++ b/tests/common/types.py
@@ -19,6 +19,17 @@ class SpaceDesc:
         self.charset = charset
         self.collate = collate
 
+    @staticmethod
+    def from_json(obj: dict):
+        return SpaceDesc(
+            name=obj.get('name', None),
+            vid_type=obj.get('vidType', 'FIXED_STRING(32)'),
+            partition_num=obj.get('partitionNum', 7),
+            replica_factor=obj.get('replicaFactor', 1),
+            charset=obj.get('charset', 'utf8'),
+            collate=obj.get('collate', 'utf8_bin'),
+        )
+
     def create_stmt(self) -> str:
         return f"""CREATE SPACE IF NOT EXISTS `{self.name}`( \
             partition_num={self.partition_num}, \
diff --git a/tests/common/utils.py b/tests/common/utils.py
index 5b9c9a280499532c7ff2ef25d69828435d54947f..645dce14647c24d1f07111c1f79ba9a94e27377c 100644
--- a/tests/common/utils.py
+++ b/tests/common/utils.py
@@ -9,10 +9,9 @@ import os
 import random
 import string
 import time
-from pathlib import Path
+import yaml
 from typing import Pattern
 
-import yaml
 from nebula2.common import ttypes as CommonTtypes
 from nebula2.gclient.net import Session
 
@@ -322,10 +321,15 @@ def space_generator(size=6, chars=string.ascii_uppercase + string.digits):
     return ''.join(random.choice(chars) for _ in range(size))
 
 
+def check_resp(resp, stmt):
+    msg = f"Fail to exec: {stmt}, error: {resp.error_msg()}"
+    assert resp.is_succeeded(), msg
+
+
 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()}"
+        check_resp(resp, stmt)
 
     exec(space_desc.drop_stmt())
     exec(space_desc.create_stmt())
@@ -336,11 +340,15 @@ def create_space(space_desc: SpaceDesc, sess: Session):
 def _load_data_from_file(sess, data_dir, fd):
     for stmt in CSVImporter(fd, data_dir):
         rs = sess.execute(stmt)
-        assert rs.is_succeeded(), \
-            f"fail to exec: {stmt}, error: {rs.error_msg()}"
+        check_resp(rs, stmt)
 
 
-def load_csv_data(pytestconfig, sess: Session, data_dir: str):
+def load_csv_data(
+    pytestconfig,
+    sess: Session,
+    data_dir: str,
+    space_name: str = "",
+):
     """
     Before loading CSV data files, you must create and select a graph
     space. The `config.yaml' file only create schema about tags and
@@ -350,12 +358,30 @@ def load_csv_data(pytestconfig, sess: Session, data_dir: str):
 
     with open(config_path, 'r') as f:
         config = yaml.full_load(f)
+
+        space = config.get('space', None)
+        assert space is not None
+        if not space_name:
+            space_name = space.get('name', "A" + space_generator())
+        space_desc = SpaceDesc(
+            name=space_name,
+            vid_type=space.get('vidType', 'FIXED_STRING(32)'),
+            partition_num=space.get('partitionNum', 7),
+            replica_factor=space.get('replicaFactor', 1),
+            charset=space.get('charset', 'utf8'),
+            collate=space.get('collate', 'utf8_bin'),
+        )
+
+        create_space(space_desc, sess)
+
         schemas = config['schema']
         stmts = ' '.join(map(lambda x: x.strip(), schemas.splitlines()))
         rs = sess.execute(stmts)
-        assert rs.is_succeeded()
+        check_resp(rs, stmts)
 
         time.sleep(3)
 
         for fd in config["files"]:
             _load_data_from_file(sess, data_dir, fd)
+
+        return space_desc
diff --git a/tests/conftest.py b/tests/conftest.py
index ab4efcc97baa3e2d595896ce721aab45d2fe3ab5..4a12e7b005084324445bbca919d596a34dd6724e 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -18,7 +18,7 @@ from nebula2.gclient.net import ConnectionPool
 from tests.common.configs import all_configs
 from tests.common.nebula_service import NebulaService
 from tests.common.types import SpaceDesc
-from tests.common.utils import create_space, load_csv_data
+from tests.common.utils import load_csv_data
 
 tests_collected = set()
 tests_executed = set()
@@ -152,46 +152,73 @@ def session(conn_pool, pytestconfig):
     sess.release()
 
 
-def load_csv_data_once(tmp_path_factory, pytestconfig, worker_id, conn_pool,
-                       space_desc: SpaceDesc):
-    space_name = space_desc.name
+def load_csv_data_once(
+    tmp_path_factory,
+    pytestconfig,
+    worker_id,
+    conn_pool: ConnectionPool,
+    space: str,
+):
     root_tmp_dir = tmp_path_factory.getbasetemp().parent
-    fn = root_tmp_dir / f"csv-data-{space_name}"
+    fn = root_tmp_dir / f"csv-data-{space}"
     is_file = True
     with FileLock(str(fn) + ".lock"):
         if not fn.is_file():
-            data_dir = os.path.join(CURR_PATH, "data", space_name)
+            data_dir = os.path.join(CURR_PATH, "data", space)
             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)
+            space_desc = load_csv_data(pytestconfig, sess, data_dir)
             sess.release()
-            fn.write_text(space_name)
+            fn.write_text(json.dumps(space_desc.__dict__))
             is_file = False
+        else:
+            space_desc = SpaceDesc.from_json(json.loads(fn.read_text()))
     if is_file:
-        logging.info(
-            f"session-{worker_id} need not to load {space_name} csv data")
+        logging.info(f"session-{worker_id} need not to load {space} csv data")
         yield space_desc
     else:
-        logging.info(f"session-{worker_id} load {space_name} csv data")
+        logging.info(f"session-{worker_id} load {space} 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):
-    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)
+    yield from load_csv_data_once(
+        tmp_path_factory,
+        pytestconfig,
+        worker_id,
+        conn_pool,
+        "nba",
+    )
+
+
+# @pytest.fixture(scope="session")
+# def load_nba_int_vid_data(
+#     conn_pool,
+#     pytestconfig,
+#     tmp_path_factory,
+#     worker_id,
+# ):
+#     yield from load_csv_data_once(
+#         tmp_path_factory,
+#         pytestconfig,
+#         worker_id,
+#         conn_pool,
+#         "nba_int_vid",
+#     )
 
 
 @pytest.fixture(scope="session")
 def load_student_data(conn_pool, pytestconfig, tmp_path_factory, worker_id):
-    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)
+    yield from load_csv_data_once(
+        tmp_path_factory,
+        pytestconfig,
+        worker_id,
+        conn_pool,
+        "student",
+    )
 
 
 # TODO(yee): Delete this when we migrate all test cases
diff --git a/tests/ntr.out-of-source b/tests/ntr.out-of-source
deleted file mode 100644
index 9a350996446fee349cec6a0d20c880dca9593d2e..0000000000000000000000000000000000000000
--- a/tests/ntr.out-of-source
+++ /dev/null
@@ -1,6 +0,0 @@
-#!@PYTHON_EXECUTABLE@
-# Call ntr in out-of-source build
-import os, sys
-os.environ['NEBULA_BUILD_DIR'] = '@CMAKE_BINARY_DIR@'
-os.environ['NEBULA_SOURCE_DIR'] = '@CMAKE_SOURCE_DIR@'
-exec(open("@CMAKE_SOURCE_DIR@/tests/nebula-test-run.py").read())
diff --git a/tests/tck/conftest.py b/tests/tck/conftest.py
index 22575c5ab78946e337fb611253690037a9b35b43..f24001c526c52b4dc6f5bc05cd55c54d4280b5d5 100644
--- a/tests/tck/conftest.py
+++ b/tests/tck/conftest.py
@@ -20,6 +20,7 @@ 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
+from tests.tck.utils.nbv import murmurhash2
 
 parse = functools.partial(parsers.parse)
 rparse = functools.partial(parsers.re)
@@ -31,10 +32,18 @@ def graph_spaces():
 
 
 @given(parse('a graph with space named "{space}"'))
-def preload_space(space, load_nba_data, load_student_data, session,
-                  graph_spaces):
+def preload_space(
+    space,
+    load_nba_data,
+    # load_nba_int_vid_data,
+    load_student_data,
+    session,
+    graph_spaces,
+):
     if space == "nba":
         graph_spaces["space_desc"] = load_nba_data
+    # elif space == "nba_int_vid":
+    #     graph_spaces["space_desc"] = load_nba_int_vid_data
     elif space == "student":
         graph_spaces["space_desc"] = load_student_data
     else:
@@ -74,21 +83,24 @@ def new_space(options, session, graph_spaces):
     graph_spaces["drop_space"] = True
 
 
-@given(parse('import "{data}" csv data'))
+@given(parse('load "{data}" csv data to a new space'))
 def import_csv_data(data, graph_spaces, session, pytestconfig):
     data_dir = os.path.join(DATA_DIR, data)
-    space_desc = graph_spaces["space_desc"]
+    space_desc = load_csv_data(
+        pytestconfig,
+        session,
+        data_dir,
+        "I" + space_generator(),
+    )
     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)
+    graph_spaces["space_desc"] = space_desc
 
 
 @when(parse("executing query:\n{query}"))
 def executing_query(query, graph_spaces, session):
     ngql = " ".join(query.splitlines())
     graph_spaces['result_set'] = session.execute(ngql)
+    graph_spaces['ngql'] = ngql
 
 
 @given(parse("wait {secs:d} seconds"))
@@ -103,16 +115,36 @@ def cmp_dataset(graph_spaces,
                 strict: bool,
                 included=False) -> None:
     rs = graph_spaces['result_set']
+    ngql = graph_spaces['ngql']
+    space_desc = graph_spaces['space_desc']
     assert rs.is_succeeded(), f"Response failed: {rs.error_msg()}"
+    vid_fn = murmurhash2 if space_desc.vid_type == 'int' else None
     ds = dataset(table(result))
     dscmp = DataSetComparator(strict=strict,
                               order=order,
                               included=included,
-                              decode_type=rs._decode_type)
-    dsp = DataSetPrinter(rs._decode_type)
-    resp_ds = rs._data_set_wrapper._data_set
-    assert dscmp(resp_ds, ds), \
-        f"Response: {dsp.ds_to_string(resp_ds)} vs. Expected: {dsp.ds_to_string(ds)}"
+                              decode_type=rs._decode_type,
+                              vid_fn=vid_fn)
+
+    def dsp(ds):
+        printer = DataSetPrinter(rs._decode_type)
+        return printer.ds_to_string(ds)
+
+    def rowp(ds, i):
+        if i is None or i < 0:
+            return ""
+        assert i < len(ds.rows), f"{i} out of range {len(ds.rows)}"
+        row = ds.rows[i].values
+        printer = DataSetPrinter(rs._decode_type)
+        ss = printer.list_to_string(row, delimiter='|')
+        return f'{i}: |' + ss + '|'
+
+    if rs._data_set_wrapper is None:
+        assert not ds.column_names and not ds.rows, f"Expected result must be empty table: ||"
+
+    rds = rs._data_set_wrapper._data_set
+    res, i = dscmp(rds, ds)
+    assert res, f"Fail to exec: {ngql}\nResponse: {dsp(rds)}\nExpected: {dsp(ds)}\nNotFoundRow: {rowp(ds, i)}"
 
 
 @then(parse("the result should be, in order:\n{result}"))
@@ -151,10 +183,9 @@ def execution_should_be_succ(graph_spaces):
     assert rs is not None, "Please execute a query at first"
     assert rs.is_succeeded(), f"Response failed: {rs.error_msg()}"
 
-@then(
-    rparse("a (?P<err_type>\w+) should be raised at (?P<time>runtime|compile time):(?P<msg>.*)")
-)
-def raised_type_error(err_type, time, msg, graph_spaces):
+
+@then(rparse(r"a (?P<err_type>\w+) should be raised at (?P<time>runtime|compile time)(?P<sym>:|.)(?P<msg>.*)"))
+def raised_type_error(err_type, time, sym, msg, graph_spaces):
     res = graph_spaces["result_set"]
     assert not res.is_succeeded(), "Response should be failed"
     err_type = err_type.strip()
diff --git a/tests/tck/features/parser/Example.feature b/tests/tck/features/parser/Example.feature
index d0f8b3dcbe51030f6f87c9de6d169ab682bbe718..bb48cf382b9ad778edc1a03062f3374077f17c6b 100644
--- a/tests/tck/features/parser/Example.feature
+++ b/tests/tck/features/parser/Example.feature
@@ -2,13 +2,7 @@ Feature: Feature examples
 
   Scenario: Supported features
     Given an empty graph
-    And create a space with following options:
-      | partition_num  | 9                |
-      | replica_factor | 1                |
-      | vid_type       | FIXED_STRING(30) |
-      | charset        | utf8             |
-      | collate        | utf8_bin         |
-    And import "nba" csv data
+    And load "nba" csv data to a new space
     And having executed:
       """
       CREATE TAG IF NOT EXISTS `test_tag`(name string)
@@ -43,5 +37,20 @@ Feature: Feature examples
       """
       CREATE TAG player(name string);
       """
-    Then a ExecutionError should be raised at runtime:
+    Then a ExecutionError should be raised at runtime.
+    Then drop the used space
+
+  Scenario: Supported space creation
+    Given an empty graph
+    And create a space with following options:
+      | partition_num  | 9                |
+      | replica_factor | 1                |
+      | vid_type       | FIXED_STRING(30) |
+      | charset        | utf8             |
+      | collate        | utf8_bin         |
+    When executing query:
+      """
+      SHOW SPACES
+      """
+    Then the execution should be successful
     Then drop the used space
diff --git a/tests/tck/utils/mmh2.py b/tests/tck/utils/mmh2.py
index 7bfea69bc4929a5cbc59949dd50aa0a14c786ddb..cb65409a4dbc0d3847782e2d39d7c01c48d66e20 100644
--- a/tests/tck/utils/mmh2.py
+++ b/tests/tck/utils/mmh2.py
@@ -1,7 +1,13 @@
+# 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.
+
 def __bytes2ul(b):
     return int.from_bytes(b, byteorder='little', signed=False)
 
-def mmh2(bstr, seed = 0xc70f6907, signed = True):
+
+def mmh2(bstr, seed=0xc70f6907, signed=True):
     MASK = 2 ** 64 - 1
     size = len(bstr)
     m = 0xc6a4a7935bd1e995
@@ -42,6 +48,7 @@ def mmh2(bstr, seed = 0xc70f6907, signed = True):
 
     return h
 
+
 if __name__ == '__main__':
     assert mmh2(b'hello') == 2762169579135187400
     assert mmh2(b'World') == -295471233978816215
diff --git a/tests/tck/utils/nbv.py b/tests/tck/utils/nbv.py
index 2f5a9acba6f1f66d19fa0a2d4d61a7d4c1cffa9c..cafe47a94a599b29f4d9d98ebee7effff4dd7fa7 100644
--- a/tests/tck/utils/nbv.py
+++ b/tests/tck/utils/nbv.py
@@ -7,7 +7,11 @@
 import re
 import ply.lex as lex
 import ply.yacc as yacc
-from tests.tck.utils.mmh2 import mmh2
+
+if __name__ == "__main__":
+    from mmh2 import mmh2
+else:
+    from tests.tck.utils.mmh2 import mmh2
 
 from nebula2.common.ttypes import (
     Value,
@@ -528,12 +532,15 @@ lexer = lex.lex()
 parser = yacc.yacc()
 functions = {}
 
+
 def murmurhash2(v):
     if isinstance(v, Value):
         v = v.get_sVal()
-    else:
-        assert isinstance(v, str)
-    return mmh2(bytes(v, 'utf-8'))
+    if type(v) is str:
+        return mmh2(bytes(v, 'utf-8'))
+    if type(v) is bytes:
+        return mmh2(v)
+    raise ValueError(f"Invalid value type: {type(v)}")
 
 
 def register_function(name, func):