diff --git a/test/test_application.rb b/test/test_application.rb
new file mode 100644
index 0000000..4fcec4e
--- /dev/null
+++ b/test/test_application.rb
@@ -0,0 +1,136 @@
+require 'minitest/autorun'
+require 'prism'
+require 'support'
+
+class SampleApp < Prism::Application
+ enable_sessions(secret: 'test-secret')
+
+ before '/admin/*' do
+ halt(401, 'unauthorized') unless params['token'] == 'let-me-in'
+ end
+
+ get '/' do
+ 'root'
+ end
+
+ get '/users/:id' do
+ json(id: params['id'])
+ end
+
+ get '/search' do
+ json(q: params['q'])
+ end
+
+ post '/echo' do
+ json(got: params.to_h)
+ end
+
+ get '/old' do
+ redirect '/new', 301
+ end
+
+ get '/greet/:name' do
+ erb('
Hi <%= name %>
', locals: { name: params['name'] })
+ end
+
+ get '/admin/panel' do
+ json(secret: 'shown')
+ end
+
+ get '/count' do
+ session['count'] = (session['count'] || 0) + 1
+ json(count: session['count'])
+ end
+
+ get '/first' do
+ pass if params['skip'] == 'yes'
+ 'first'
+ end
+
+ get '/first' do
+ 'second'
+ end
+end
+
+class ApplicationTest < Minitest::Test
+ include TestSupport
+
+ def call(method, path, **opts)
+ SampleApp.call(env_for(method, path, **opts))
+ end
+
+ def test_root
+ status, _, body = call('GET', '/')
+ assert_equal 200, status
+ assert_equal 'root', body.join
+ end
+
+ def test_path_param
+ status, headers, body = call('GET', '/users/77')
+ assert_equal 200, status
+ assert_equal 'application/json', headers['Content-Type']
+ assert_equal({ 'id' => '77' }, JSON.parse(body.join))
+ end
+
+ def test_query_param
+ _, _, body = call('GET', '/search?q=prism')
+ assert_equal({ 'q' => 'prism' }, JSON.parse(body.join))
+ end
+
+ def test_post_json
+ payload = JSON.generate(name: 'x', n: 5)
+ _, _, body = call('POST', '/echo', headers: { 'Content-Type' => 'application/json' }, body: payload)
+ assert_equal({ 'name' => 'x', 'n' => 5 }, JSON.parse(body.join)['got'])
+ end
+
+ def test_redirect
+ status, headers, = call('GET', '/old')
+ assert_equal 301, status
+ assert_equal '/new', headers['Location']
+ end
+
+ def test_not_found
+ status, _, body = call('GET', '/missing')
+ assert_equal 404, status
+ assert_includes body.join, 'Not Found'
+ end
+
+ def test_template_escaping
+ _, _, body = call('GET', '/greet/%3Cb%3E')
+ assert_equal 'Hi <b>
', body.join
+ end
+
+ def test_before_filter_blocks
+ status, _, body = call('GET', '/admin/panel')
+ assert_equal 401, status
+ assert_equal 'unauthorized', body.join
+ end
+
+ def test_before_filter_allows
+ status, _, body = call('GET', '/admin/panel?token=let-me-in')
+ assert_equal 200, status
+ assert_equal 'shown', JSON.parse(body.join)['secret']
+ end
+
+ def test_session_write_and_read
+ _, headers, body = call('GET', '/count')
+ assert_equal 1, JSON.parse(body.join)['count']
+ cookie = headers['Set-Cookie'].split(';').first
+ name, value = cookie.split('=', 2)
+ _, _, body2 = call('GET', '/count', cookies: "#{name}=#{value}")
+ assert_equal 2, JSON.parse(body2.join)['count']
+ end
+
+ def test_pass
+ _, _, body = call('GET', '/first?skip=yes')
+ assert_equal 'second', body.join
+ _, _, second = call('GET', '/first')
+ assert_equal 'first', second.join
+ end
+
+ def test_head_has_empty_body
+ status, _, body = call('HEAD', '/')
+ assert_equal 200, status
+ assert_equal '', body.join
+ end
+end
diff --git a/test/test_application_routing.rb b/test/test_application_routing.rb
new file mode 100644
index 0000000..810a272
--- /dev/null
+++ b/test/test_application_routing.rb
@@ -0,0 +1,100 @@
+require 'minitest/autorun'
+require 'prism'
+require 'support'
+
+class ResourceApp < Prism::Application
+ enable_sessions(secret: 'routing-secret')
+
+ root(to: 'welcome#show')
+
+ resources :routing_posts, only: %i[index show create]
+
+ get '/whoami' do
+ json(path: url_for(:routing_post, id: 5))
+ end
+
+ get '/write-flash' do
+ flash[:notice] = 'saved!'
+ redirect '/after-flash'
+ end
+
+ get '/after-flash' do
+ json(notice: flash[:notice])
+ end
+end
+
+class WelcomeController < Prism::Controller
+ def show
+ html('welcome')
+ end
+end
+
+class RoutingPostsController < Prism::Controller
+ def index
+ json(action: 'index')
+ end
+
+ def show
+ json(action: 'show', id: params['id'])
+ end
+
+ def create
+ json(action: 'create')
+ end
+end
+
+class ApplicationRoutingTest < Minitest::Test
+ include TestSupport
+
+ def call(method, path, **opts)
+ ResourceApp.call(env_for(method, path, **opts))
+ end
+
+ def test_root_dispatches_to_controller
+ status, _, body = call('GET', '/')
+ assert_equal 200, status
+ assert_equal 'welcome', body.join
+ end
+
+ def test_resources_index
+ _, _, body = call('GET', '/routing_posts')
+ assert_equal({ 'action' => 'index' }, JSON.parse(body.join))
+ end
+
+ def test_resources_show
+ _, _, body = call('GET', '/routing_posts/9')
+ assert_equal({ 'action' => 'show', 'id' => '9' }, JSON.parse(body.join))
+ end
+
+ def test_resources_create
+ _, _, body = call('POST', '/routing_posts')
+ assert_equal({ 'action' => 'create' }, JSON.parse(body.join))
+ end
+
+ def test_resources_only_registers_requested_actions
+ status, = call('DELETE', '/routing_posts/1')
+ assert_equal 404, status
+ end
+
+ def test_url_for_named_resource_route
+ _, _, body = call('GET', '/whoami')
+ assert_equal '/routing_posts/5', JSON.parse(body.join)['path']
+ end
+
+ def test_flash_survives_a_redirect
+ status, headers, = call('GET', '/write-flash')
+ assert_equal 302, status
+ cookie = headers['Set-Cookie'].split(';').first
+ _, _, body = call('GET', '/after-flash', cookies: cookie)
+ assert_equal 'saved!', JSON.parse(body.join)['notice']
+ end
+
+ def test_flash_does_not_survive_a_second_request
+ _, headers, = call('GET', '/write-flash')
+ cookie = headers['Set-Cookie'].split(';').first
+ _, headers2, = call('GET', '/after-flash', cookies: cookie)
+ cookie2 = headers2['Set-Cookie']&.split(';')&.first || cookie
+ _, _, body = call('GET', '/after-flash', cookies: cookie2)
+ assert_nil JSON.parse(body.join)['notice']
+ end
+end