diff --git a/vlib/vweb/tests/vweb_test.v b/vlib/vweb/tests/vweb_test.v
new file mode 100644
index 0000000000..83dc58c3bc
--- /dev/null
+++ b/vlib/vweb/tests/vweb_test.v
@@ -0,0 +1,189 @@
+import os
+import time
+import net
+import net.http
+
+const (
+ sport = 12380
+ exit_after_time = 1000 // milliseconds
+ vexe = os.getenv('VEXE')
+ vroot = os.dir(vexe)
+ serverexe = os.join_path(os.cache_dir(), 'vweb_test_server.exe')
+)
+
+// setup of vweb webserver
+fn testsuite_begin() {
+ os.chdir(vroot)
+ if os.exists(serverexe) {
+ os.rm(serverexe)
+ }
+ // prevent failing tests when vweb_test.v is rerun quickly
+ // and the previous webserver has not yet timed out.
+ for i := 0; i < 10; i++ {
+ if client := net.dial('127.0.0.1', sport) {
+ client.close() or { }
+ eprintln('previous webserver has not yet stopped ($i); waiting...')
+ time.sleep_ms(exit_after_time / 10)
+ continue
+ } else {
+ return
+ }
+ }
+}
+
+fn test_a_simple_vweb_app_can_be_compiled() {
+ did_server_compile := os.system('$vexe -o $serverexe vlib/vweb/tests/vweb_test_server.v')
+ assert did_server_compile == 0
+ assert os.exists(serverexe)
+}
+
+fn test_a_simple_vweb_app_runs_in_the_background() {
+ server_exec_cmd := '$serverexe $sport $exit_after_time > /dev/null &'
+ $if debug_net_socket_client ? {
+ eprintln('running:\n$server_exec_cmd')
+ }
+ res := os.system(server_exec_cmd)
+ assert res == 0
+ time.sleep_ms(100)
+}
+
+// web client tests follow
+fn assert_common_headers(received string) {
+ assert received.starts_with('HTTP/1.1 200 OK\r\n')
+ assert received.contains('Server: VWeb\r\n')
+ assert received.contains('Content-Length:')
+ assert received.contains('Connection: close\r\n')
+}
+
+fn test_a_simple_tcp_client_can_connect_to_the_vweb_server() {
+ received := simple_tcp_client({}) or {
+ assert err == ''
+ return
+ }
+ assert_common_headers(received)
+ assert received.contains('Content-Type: text/plain')
+ assert received.contains('Content-Length: 15')
+ assert received.ends_with('Welcome to VWeb')
+}
+
+fn test_a_simple_tcp_client_simple_route() {
+ received := simple_tcp_client({
+ path: '/simple'
+ }) or {
+ assert err == ''
+ return
+ }
+ assert_common_headers(received)
+ assert received.contains('Content-Type: text/plain')
+ assert received.contains('Content-Length: 15')
+ assert received.ends_with('A simple result')
+}
+
+fn test_a_simple_tcp_client_html_page() {
+ received := simple_tcp_client({
+ path: '/html_page'
+ }) or {
+ assert err == ''
+ return
+ }
+ assert_common_headers(received)
+ assert received.contains('Content-Type: text/html')
+ assert received.ends_with('
ok
')
+}
+
+// net.http client based tests follow:
+fn assert_common_http_headers(x http.Response) {
+ assert x.status_code == 200
+ assert x.headers['Server'] == 'VWeb'
+ assert x.headers['Content-Length'].int() > 0
+ assert x.headers['Connection'] == 'close'
+}
+
+fn test_http_client_index() {
+ x := http.get('http://127.0.0.1:$sport/') or {
+ panic(err)
+ }
+ assert_common_http_headers(x)
+ assert x.headers['Content-Type'] == 'text/plain'
+ assert x.text == 'Welcome to VWeb'
+}
+
+fn test_http_client_simple() {
+ x := http.get('http://127.0.0.1:$sport/simple') or {
+ panic(err)
+ }
+ assert_common_http_headers(x)
+ assert x.headers['Content-Type'] == 'text/plain'
+ assert x.text == 'A simple result'
+}
+
+fn test_http_client_html_page() {
+ x := http.get('http://127.0.0.1:$sport/html_page') or {
+ panic(err)
+ }
+ assert_common_http_headers(x)
+ assert x.headers['Content-Type'] == 'text/html'
+ assert x.text == 'ok
'
+}
+
+fn test_http_client_settings_page() {
+ x := http.get('http://127.0.0.1:$sport/bilbo/settings') or {
+ panic(err)
+ }
+ assert_common_http_headers(x)
+ assert x.text == 'username: bilbo'
+ y := http.get('http://127.0.0.1:$sport/kent/settings') or {
+ panic(err)
+ }
+ assert_common_http_headers(y)
+ assert y.text == 'username: kent'
+}
+
+fn test_http_client_user_repo_settings_page() {
+ x := http.get('http://127.0.0.1:$sport/bilbo/gostamp/settings') or {
+ panic(err)
+ }
+ assert_common_http_headers(x)
+ assert x.text == 'username: bilbo | repository: gostamp'
+ y := http.get('http://127.0.0.1:$sport/kent/golang/settings') or {
+ panic(err)
+ }
+ assert_common_http_headers(y)
+ assert y.text == 'username: kent | repository: golang'
+}
+
+// utility code:
+struct SimpleTcpClientConfig {
+ host string = 'static.dev'
+ path string = '/'
+ agent string = 'v/net.tcp.v'
+ headers string = ''
+ content string = ''
+}
+
+fn simple_tcp_client(config SimpleTcpClientConfig) ?string {
+ client := net.dial('127.0.0.1', sport) or {
+ return error(err)
+ }
+ defer {
+ client.close() or { }
+ }
+ message := 'GET $config.path HTTP/1.1
+Host: $config.host
+User-Agent: $config.agent
+Accept: */*
+$config.headers
+$config.content'
+ $if debug_net_socket_client ? {
+ eprintln('sending:\n$message')
+ }
+ client.send(message.str, message.len) or {
+ return error(err)
+ }
+ bytes, blen := client.recv(4096)
+ received := unsafe {bytes.vstring_with_len(blen)}
+ $if debug_net_socket_client ? {
+ eprintln('received:\n$received')
+ }
+ return received
+}
diff --git a/vlib/vweb/tests/vweb_test_server.v b/vlib/vweb/tests/vweb_test_server.v
new file mode 100644
index 0000000000..f4508095f8
--- /dev/null
+++ b/vlib/vweb/tests/vweb_test_server.v
@@ -0,0 +1,69 @@
+module main
+
+import os
+import vweb
+import time
+
+struct App {
+ port int
+ timeout int
+pub mut:
+ vweb vweb.Context
+}
+
+fn exit_after_timeout(timeout_in_ms int) {
+ time.sleep_ms(timeout_in_ms)
+ // eprintln('webserver is exiting ...')
+ exit(0)
+}
+
+fn main() {
+ if os.args.len != 3 {
+ panic('Usage: `vweb_test_server.exe PORT TIMEOUT_IN_MILLISECONDS`')
+ }
+ http_port := os.args[1].int()
+ assert http_port > 0
+ timeout := os.args[2].int()
+ assert timeout > 0
+ go exit_after_timeout(timeout)
+ //
+ mut app := App{
+ port: http_port
+ timeout: timeout
+ }
+ vweb.run_app(mut app, http_port)
+}
+
+pub fn (mut app App) init() {
+}
+
+pub fn (mut app App) init_once() {
+ eprintln('Started webserver on http://127.0.0.1:$app.port/ , with maximum runtime of $app.timeout milliseconds.')
+}
+
+pub fn (mut app App) index() {
+ app.vweb.text('Welcome to VWeb')
+}
+
+pub fn (mut app App) simple() vweb.Result {
+ app.vweb.text('A simple result')
+ return vweb.Result{}
+}
+
+pub fn (mut app App) html_page() vweb.Result {
+ app.vweb.html('ok
')
+ return vweb.Result{}
+}
+
+// the following serve custom routes
+['/:user/settings']
+pub fn (mut app App) settings(username string) vweb.Result {
+ app.vweb.html('username: $username')
+ return vweb.Result{}
+}
+
+['/:user/:repo/settings']
+pub fn (mut app App) user_repo_settings(username, repository string) vweb.Result {
+ app.vweb.html('username: $username | repository: $repository')
+ return vweb.Result{}
+}