NAME

Test2::Thunderhorse - Test2-native testing tools for Thunderhorse applications

SYNOPSIS

use v5.40;
use Test2::V1;
use Test2::Thunderhorse;
use HTTP::Request::Common;

package MyApp {
	use Mooish::Base -standard;
	extends 'Thunderhorse::App';

	sub build ($self) {
		$self->router->add(
			'/hello' => {
				to => sub ($self, $ctx) {
					return 'Hello World';
				}
			}
		);
	}
}

my $app = MyApp->new;

# Test HTTP endpoints
http $app, GET '/hello';
http_status_is 200;
http_header_is 'Content-Type', 'text/html; charset=utf-8';
http_text_is 'Hello World';

# Test WebSocket connections
websocket $app, '/ws/echo';
websocket->send_text('test');
is websocket->receive_text, 'echo: test';
websocket->close;

# Test Server-Sent Events
sse $app, '/events';
is sse->receive_event->{data}, 'message';
sse->close;

done_testing;

DESCRIPTION

Test2::Thunderhorse provides a Test2-native interface for testing Thunderhorse applications. It wraps PAGI::Test::Client and provides convenient exported functions for testing HTTP requests, WebSocket connections, and Server-Sent Events streams.

The module automatically sets PAGI_ENV to test and prevents loading in non-test environments. All exported functions integrate with Test2's context system for proper test result reporting.

EXPORTED FUNCTIONS

pagi_run

my $state = pagi_run $app, $coderef;

Fires a PAGI startup lifecycle event, then executes the $coderef. This triggers on_startup and on_shutdown hooks in Thunderhorse, and may be mandatory depending on the app configuration. Inside $coderef, test the app normally using other functions.

Returns the shared state from lifespan.

http

http $app, GET '/path';
http $app, POST '/path', Content => 'data';

my $response = http;

Makes an HTTP request to the application and stores the response as the current HTTP response. When called without arguments, returns the last HTTP response object.

The first argument is the Thunderhorse application object. The second argument is an HTTP::Request object, typically created using HTTP::Request::Common functions like GET, POST, PUT, DELETE, etc.

The returned response object is a PAGI::Test::Response object with methods like status, text, json, header, etc.

http_status_is

http_status_is 200;
http_status_is 404;

Tests that the last HTTP response status code matches the expected value. This is a Test2 assertion that will pass or fail appropriately.

This helper is here to test a common case of comparing the HTTP status code. It works the same as is http->status, $status, 'status ok'. Use http->status with other Test2 tools to do more complex comparisons.

http_header_is

http_header_is 'Content-Type', 'text/html; charset=utf-8';
http_header_is 'X-Custom-Header', 'value';

Tests that a specific header in the last HTTP response matches the expected value. This is a Test2 assertion.

This helper is here to test a common case of comparing the HTTP header's value. It works the same as is http->header($header), $value, "$header header ok". Use http->header with other Test2 tools to do more complex comparisons.

http_text_is

http_text_is 'Hello World';
http_text_is '{"status":"ok"}';

Tests that the body of the last HTTP response matches the expected value. This is a Test2 assertion.

This helper is here to test a common case of comparing the HTTP body. It works the same as is http->text, $body, 'body ok'. Use http->text with other Test2 tools to do more complex comparisons.

websocket

websocket $app, '/ws/path';
websocket $app, '/ws/path', headers => {...};

my $ws = websocket;

Opens a WebSocket connection to the application and stores it as the current WebSocket connection. When called without arguments, returns the last WebSocket connection object.

The first argument is the Thunderhorse application object. The second argument is the WebSocket endpoint path. Additional arguments are passed as options to the underlying client.

If the WebSocket connection fails to establish, a test failure is recorded.

The returned WebSocket object is a PAGI::Test::WebSocket object with methods like:

  • send_text($text) - Send text message

  • send_json($data) - Send JSON message

  • receive_text - Receive text message

  • receive_json - Receive and decode JSON message

  • close - Close the connection

  • is_closed - Check if connection is closed

sse

sse $app, '/events/path';
sse $app, '/events/path', headers => {...};

my $sse = sse;

Opens a Server-Sent Events connection to the application and stores it as the current SSE connection. When called without arguments, returns the last SSE connection object.

The first argument is the Thunderhorse application object. The second argument is the SSE endpoint path. Additional arguments are passed as options to the underlying client.

If the SSE connection fails to establish, a test failure is recorded.

The returned SSE object is a PAGI::Test::SSE object with methods like:

  • receive_event - Receive next event as hashref with keys: data, event, id

  • receive_json - Receive and decode JSON from next event's data

  • close - Close the connection

  • is_closed - Check if connection is closed

TESTING PATTERNS

Basic HTTP Testing

subtest 'should handle GET request' => sub {
	http $app, GET '/api/users';
	http_status_is 200;
	http_header_is 'Content-Type', 'application/json; charset=utf-8';

	my $data = http->json;
	is scalar($data->@*), 3, 'got 3 users';
};

Testing with POST Data

subtest 'should create user' => sub {
	http $app, POST '/api/users',
		Content_Type => 'application/json',
		Content => '{"name":"John"}';

	http_status_is 201;
	http_text_is '{"status":"created"}';
};

WebSocket Testing

subtest 'should echo messages' => sub {
	websocket $app, '/ws/echo';

	websocket->send_text('hello');
	is websocket->receive_text, 'echo: hello';

	websocket->send_json({msg => 'test'});
	is websocket->receive_json, {msg => 'test', echoed => 1};

	websocket->close;
	ok websocket->is_closed;
};

SSE Testing

subtest 'should stream events' => sub {
	sse $app, '/events/stream';

	my $event = sse->receive_event;
	is $event->{data}, 'first message';
	is $event->{event}, 'update';
	is $event->{id}, 1;

	my $json = sse->receive_json;
	is $json->{count}, 42;

	sse->close;
};

ENVIRONMENT

The module automatically sets PAGI_ENV to test when loaded and will die if loaded in an environment where PAGI_ENV is already set to something other than test. This ensures tests always run in test mode.

SEE ALSO

Test2::V1, PAGI::Test::Client, Thunderhorse, HTTP::Request::Common

AUTHOR

Bartosz Jarzyna <bbrtj.pro@gmail.com>

COPYRIGHT AND LICENSE

Copyright (C) 2025 by Bartosz Jarzyna

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.