diff --git a/.ci/check_source_file_changes.ts b/.ci/check_source_file_changes.ts
new file mode 100644
index 000000000000..e96a16b626b4
--- /dev/null
+++ b/.ci/check_source_file_changes.ts
@@ -0,0 +1,32 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { xrun } from "../prettier/util.ts";
+import { red, green } from "../colors/mod.ts";
+
+/**
+ * Checks whether any source file is changed since the given start time.
+ * If some files are changed, this function exits with 1.
+ */
+async function main(startTime: number): Promise<void> {
+  console.log("test checkSourceFileChanges ...");
+  const changed = new TextDecoder()
+    .decode(await xrun({ args: ["git", "ls-files"], stdout: "piped" }).output())
+    .trim()
+    .split("\n")
+    .filter(file => {
+      const stat = Deno.lstatSync(file);
+      if (stat != null) {
+        return (stat as any).modified * 1000 > startTime;
+      }
+    });
+  if (changed.length > 0) {
+    console.log(red("FAILED"));
+    console.log(
+      `Error: Some source files are modified during test: ${changed.join(", ")}`
+    );
+    Deno.exit(1);
+  } else {
+    console.log(green("ok"));
+  }
+}
+
+main(parseInt(Deno.args[1]));
diff --git a/.ci/template.common.yml b/.ci/template.common.yml
index 37813cd4e7c0..19ac964a0ec7 100644
--- a/.ci/template.common.yml
+++ b/.ci/template.common.yml
@@ -3,4 +3,6 @@ parameters:
 
 steps:
   - bash: deno${{ parameters.exe_suffix }} run --allow-run --allow-write --allow-read --allow-env ./format.ts --check
-  - bash: deno${{ parameters.exe_suffix }} run --allow-run --allow-net --allow-write --allow-read --allow-env --config=tsconfig.test.json ./test.ts
\ No newline at end of file
+  - bash: export START_TIME=$(date +%s)
+  - bash: deno${{ parameters.exe_suffix }} run --allow-run --allow-net --allow-write --allow-read --allow-env --config=tsconfig.test.json ./testing/runner.ts --exclude node_modules
+  - bash: deno${{ parameters.exe_suffix }} run --allow-run --allow-read .ci/check_source_file_changes.ts $START_TIME
\ No newline at end of file
diff --git a/encoding/test.ts b/encoding/test.ts
deleted file mode 100644
index c7a1c9716007..000000000000
--- a/encoding/test.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
-import "./hex_test.ts";
-import "./toml_test.ts";
-import "./csv_test.ts";
diff --git a/flags/test.ts b/flags/test.ts
deleted file mode 100644
index f8e928555bd8..000000000000
--- a/flags/test.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
-import "./all_bool_test.ts";
-import "./bool_test.ts";
-import "./dash_test.ts";
-import "./default_bool_test.ts";
-import "./dotted_test.ts";
-import "./kv_short_test.ts";
-import "./long_test.ts";
-import "./num_test.ts";
-import "./parse_test.ts";
-import "./short_test.ts";
-import "./stop_early_test.ts";
-import "./unknown_test.ts";
-import "./whitespace_test.ts";
diff --git a/fs/test.ts b/fs/test.ts
deleted file mode 100644
index 43d6550b89b1..000000000000
--- a/fs/test.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
-import "./path/test.ts";
-import "./walk_test.ts";
-import "./globrex_test.ts";
-import "./glob_test.ts";
-import "./exists_test.ts";
-import "./eol_test.ts";
-import "./empty_dir_test.ts";
-import "./ensure_dir_test.ts";
-import "./ensure_file_test.ts";
-import "./ensure_symlink_test.ts";
-import "./ensure_link_test.ts";
-import "./move_test.ts";
-import "./copy_test.ts";
-import "./read_json_test.ts";
-import "./write_json_test.ts";
-import "./read_file_str_test.ts";
-import "./write_file_str_test.ts";
-import "./utils_test.ts";
diff --git a/http/test.ts b/http/test.ts
deleted file mode 100644
index 7226ad40fea4..000000000000
--- a/http/test.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
-import "./cookie_test.ts";
-import "./server_test.ts";
-import "./file_server_test.ts";
-import "./racing_server_test.ts";
diff --git a/io/test.ts b/io/test.ts
deleted file mode 100644
index a5c942aef9ea..000000000000
--- a/io/test.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
-import "./bufio_test.ts";
-import "./ioutil_test.ts";
-import "./util_test.ts";
-import "./writers_test.ts";
-import "./readers_test.ts";
diff --git a/log/test.ts b/log/test.ts
index 9be1ec2a1f59..5a17f9a358b8 100644
--- a/log/test.ts
+++ b/log/test.ts
@@ -4,9 +4,6 @@ import { assertEquals } from "../testing/asserts.ts";
 import * as log from "./mod.ts";
 import { LogLevel } from "./levels.ts";
 
-import "./handlers_test.ts";
-import "./logger_test.ts";
-
 class TestHandler extends log.handlers.BaseHandler {
   public messages: string[] = [];
 
diff --git a/mime/test.ts b/mime/test.ts
deleted file mode 100644
index e8bbfada9604..000000000000
--- a/mime/test.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
-import "./multipart_test.ts";
diff --git a/multipart/test.ts b/multipart/test.ts
deleted file mode 100644
index 9adde5158e09..000000000000
--- a/multipart/test.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
-import "./formfile_test.ts";
diff --git a/prettier/test.ts b/prettier/test.ts
deleted file mode 100644
index d2d1eabd75df..000000000000
--- a/prettier/test.ts
+++ /dev/null
@@ -1 +0,0 @@
-import "./main_test.ts";
diff --git a/strings/test.ts b/strings/test.ts
deleted file mode 100644
index d0d96aa08345..000000000000
--- a/strings/test.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
-import "./pad_test.ts";
diff --git a/test.ts b/test.ts
deleted file mode 100755
index 8e292191e4ef..000000000000
--- a/test.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-#!/usr/bin/env -S deno run -A
-// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
-import "./archive/tar_test.ts";
-import "./bytes/test.ts";
-import "./bundle/test.ts";
-import "./colors/test.ts";
-import "./datetime/test.ts";
-import "./encoding/test.ts";
-import "./examples/test.ts";
-import "./flags/test.ts";
-import "./fs/test.ts";
-import "./http/test.ts";
-import "./io/test.ts";
-import "./installer/test.ts";
-import "./log/test.ts";
-import "./media_types/test.ts";
-import "./mime/test.ts";
-import "./multipart/test.ts";
-import "./prettier/test.ts";
-import "./strings/test.ts";
-import "./testing/test.ts";
-import "./textproto/test.ts";
-import "./util/test.ts";
-import "./uuid/test.ts";
-import "./ws/test.ts";
-import "./encoding/test.ts";
-
-import { xrun } from "./prettier/util.ts";
-import { red, green } from "./colors/mod.ts";
-import { runTests } from "./testing/mod.ts";
-
-async function run(): Promise<void> {
-  const startTime = Date.now();
-  await runTests();
-  await checkSourceFileChanges(startTime);
-}
-
-/**
- * Checks whether any source file is changed since the given start time.
- * If some files are changed, this function exits with 1.
- */
-async function checkSourceFileChanges(startTime: number): Promise<void> {
-  console.log("test checkSourceFileChanges ...");
-  const changed = new TextDecoder()
-    .decode(await xrun({ args: ["git", "ls-files"], stdout: "piped" }).output())
-    .trim()
-    .split("\n")
-    .filter(file => {
-      const stat = Deno.lstatSync(file);
-      if (stat != null) {
-        return (stat as any).modified * 1000 > startTime;
-      }
-    });
-  if (changed.length > 0) {
-    console.log(red("FAILED"));
-    console.log(
-      `Error: Some source files are modified during test: ${changed.join(", ")}`
-    );
-    Deno.exit(1);
-  } else {
-    console.log(green("ok"));
-  }
-}
-
-run();
diff --git a/testing/mod.ts b/testing/mod.ts
index fa6fda246dc3..d1ddb9c8ce7b 100644
--- a/testing/mod.ts
+++ b/testing/mod.ts
@@ -315,6 +315,8 @@ export interface RunOptions {
  * Runs specified test cases.
  * Parallel execution can be enabled via the boolean option; default: serial.
  */
+// TODO: change return type to `Promise<boolean>` - ie. don't
+// exit but return value
 export async function runTests({
   parallel = false,
   exitOnFail = false,
diff --git a/testing/runner.ts b/testing/runner.ts
new file mode 100755
index 000000000000..eda28e02eb4d
--- /dev/null
+++ b/testing/runner.ts
@@ -0,0 +1,127 @@
+#!/usr/bin/env deno -A
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { parse } from "../flags/mod.ts";
+import { glob, isGlob, walk } from "../fs/mod.ts";
+import { runTests } from "./mod.ts";
+const { args, cwd } = Deno;
+
+const DEFAULT_GLOBS = [
+  "**/*_test.ts",
+  "**/*_test.js",
+  "**/test.ts",
+  "**/test.js"
+];
+
+/* eslint-disable max-len */
+function showHelp(): void {
+  console.log(`Deno test runner
+
+USAGE:
+  deno -A https://deno.land/std/testing/runner.ts [OPTIONS] [FILES...]
+
+OPTIONS:
+  -q, --quiet               Don't show output from test cases 
+  -f, --failfast            Stop test suite on first error
+  -e, --exclude <FILES...>  List of file names to exclude from run. If this options is 
+                            used files to match must be specified after "--". 
+  
+ARGS:
+  [FILES...]  List of file names to run. Defaults to: ${DEFAULT_GLOBS.join(
+    ","
+  )} 
+`);
+}
+/* eslint-enable max-len */
+
+function filePathToRegExp(str: string): RegExp {
+  if (isGlob(str)) {
+    return glob(str);
+  }
+
+  return RegExp(str);
+}
+
+/**
+ * This function runs matching test files in `root` directory.
+ *
+ * File matching and excluding supports glob syntax, ie. if encountered arg is
+ * a glob it will be expanded using `glob` method from `fs` module.
+ *
+ * Note that your shell may expand globs for you:
+ *    $ deno -A ./runner.ts **\/*_test.ts **\/test.ts
+ *
+ * Expanding using `fs.glob`:
+ *    $ deno -A ./runner.ts \*\*\/\*_test.ts \*\*\/test.ts
+ *
+ *  `**\/*_test.ts` and `**\/test.ts"` are arguments that will be parsed and
+ *  expanded as: [glob("**\/*_test.ts"), glob("**\/test.ts")]
+ */
+// TODO: change return type to `Promise<void>` once, `runTests` is updated
+// to return boolean instead of exiting
+export async function main(root: string = cwd()): Promise<void> {
+  const parsedArgs = parse(args.slice(1), {
+    boolean: ["quiet", "failfast", "help"],
+    string: ["exclude"],
+    alias: {
+      help: ["h"],
+      quiet: ["q"],
+      failfast: ["f"],
+      exclude: ["e"]
+    }
+  });
+
+  if (parsedArgs.help) {
+    return showHelp();
+  }
+
+  let includeFiles: string[];
+  let excludeFiles: string[];
+
+  if (parsedArgs._.length) {
+    includeFiles = (parsedArgs._ as string[])
+      .map(
+        (fileGlob: string): string[] => {
+          return fileGlob.split(",");
+        }
+      )
+      .flat();
+  } else {
+    includeFiles = DEFAULT_GLOBS;
+  }
+
+  if (parsedArgs.exclude) {
+    excludeFiles = (parsedArgs.exclude as string).split(",");
+  } else {
+    excludeFiles = [];
+  }
+
+  const filesIterator = walk(root, {
+    match: includeFiles.map((f: string): RegExp => filePathToRegExp(f)),
+    skip: excludeFiles.map((f: string): RegExp => filePathToRegExp(f))
+  });
+
+  const foundTestFiles: string[] = [];
+  for await (const { filename } of filesIterator) {
+    foundTestFiles.push(filename);
+  }
+
+  if (foundTestFiles.length === 0) {
+    console.error("No matching test files found.");
+    return;
+  }
+
+  console.log(`Found ${foundTestFiles.length} matching test files.`);
+
+  for (const filename of foundTestFiles) {
+    await import(filename);
+  }
+
+  await runTests({
+    exitOnFail: !!parsedArgs.failfast,
+    disableLog: !!parsedArgs.quiet
+  });
+}
+
+if (import.meta.main) {
+  main();
+}
diff --git a/testing/test.ts b/testing/test.ts
index 3a35be1e9893..93233f7cc9fb 100644
--- a/testing/test.ts
+++ b/testing/test.ts
@@ -7,10 +7,6 @@ import {
   assertThrows,
   assertThrowsAsync
 } from "./asserts.ts";
-import "./format_test.ts";
-import "./diff_test.ts";
-import "./asserts_test.ts";
-import "./bench_test.ts";
 
 test(function testingAssertEqualActualUncoercable(): void {
   let didThrow = false;
diff --git a/textproto/test.ts b/textproto/test.ts
index 71caddef9a01..bdb9293699a0 100644
--- a/textproto/test.ts
+++ b/textproto/test.ts
@@ -6,7 +6,6 @@
 import { append } from "./mod.ts";
 import { assertEquals } from "../testing/asserts.ts";
 import { test } from "../testing/mod.ts";
-import "./reader_test.ts";
 
 test(async function textprotoAppend(): Promise<void> {
   const enc = new TextEncoder();
diff --git a/util/test.ts b/util/test.ts
deleted file mode 100644
index ede984904f0d..000000000000
--- a/util/test.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-import "./async_test.ts";
-import "./deep_assign_test.ts";
diff --git a/ws/test.ts b/ws/test.ts
index e14af1d559bd..b8eb42803ec9 100644
--- a/ws/test.ts
+++ b/ws/test.ts
@@ -1,5 +1,4 @@
 // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
-import "./sha1_test.ts";
 import { BufReader } from "../io/bufio.ts";
 import { assert, assertEquals } from "../testing/asserts.ts";
 import { runIfMain, test } from "../testing/mod.ts";