Skip to main content
Version: Next

Testing

This section covers the basics regarding how to test a Marten project and the various tools that you can leverage in this regard.

The basics

You should test your Marten project to ensure that it adheres to the specifications it was built for. Like any Crystal project, Marten lets you write "specs" (see the official documentation related to testing in Crystal to learn more about those).

By default, when creating a project through the use of the new management command, Marten will automatically create a spec/ folder at the root of your project structure. This folder contains a unique spec_helper.cr file allowing you to initialize the test environment for your Marten project.

This file should look something like this:

ENV["MARTEN_ENV"] = "test"

require "spec"
require "marten"
require "marten/spec"

require "../src/project"

As you can see, the spec_helper.cr file forces the Marten environment variable to be set to test and requires the spec library as well as Marten and your actual project. This file should be required by all your spec files.

info

It's very important to require marten/spec in your top-level spec helper as this will ensure that the mandatory spec callbacks are configured for your spec suite (eg. in order to ensure that your database is properly set up before each spec is executed).

When it comes to running your tests, you can simply make use of the standard crystal spec command.

Writing tests

To write tests, you should write regular specs and ensure that your spec files always require the spec/spec_helper.cr file.

For example:

require "./spec_helper"

describe MySuperAbstraction do
describe "#foo" do
it "returns bar" do
obj = MySuperAbstraction.new
obj.foo.should eq "bar"
end
end
end

You are encouraged to organize your spec files by following the structure of your projects. For example, you could create a models folder and define specs related to your models in it.

tip

When organizing spec files across multiple folders, one good practice is to define a spec_helper.cr file at each level of your folders structure. These additional spec_helper.cr files should require the same file from the parent folder.

For example:

require "../spec_helper"
require "./spec_helper"

describe Article do
# ...
end

Running tests

As mentioned before, running specs involves making use of the standard crystal spec command.

The test environment

By default, the new management command always creates a test environment when generating new projects. As such, you should ensure that the MARTEN_ENV environment variable is set to test when running your Crystal specs. It should also be reminded that this test environment is associated with a dedicated settings file where test-related settings can be specified and/or overridden if necessary (see Settings for more details about this).

The test database

Marten must use a different database when running tests in order to not tamper with your regular database. Indeed, the database used in the context of specs will be flushed and generated automatically every time the specs suite is executed. You should not set these database names to the same names as the ones used for your development or production environments. If test database names are not explicitly set, your specs suite won't be allowed to run at all.

One way to ensure you use a dedicated database specifically for tests is to override the database settings as follows:

Marten.configure :test do |config|
config.database do |db|
db.name = "my_project_test"
end
end

Testing tools

Marten provides some tools that can become useful when writing specs.

Using the test client

The test client is an abstraction that is provided when requiring marten/spec and that acts as a very basic web client. This tool allows you to easily test your handlers and the various routes of your application by issuing requests and by introspecting the returned responses.

By leveraging the test client, you can easily simulate various requests (eg. GET or POST requests) for specific URLs and observe the returned responses. While doing so, you can introspect the response properties (such as its status code, content, and headers) in order to verify that your handlers behave as expected.

A simple example

To use the test client, you can either initialize a Marten::Spec::Client object or make use of the per-spec test client that is provided by the Marten::Spec#client method. Initializing new Marten::Spec::Client objects allow you to set client-wide properties, like a default content type.

info

Note that the client returned by the Marten::Spec#client method is memoized and is reset after each spec execution.

Let's have a look at a simple way to use the test client and verify the corresponding responses:

describe MyRedirectHandler do
describe "#get" do
it "returns the expected redirect response" do
response = Marten::Spec.client.get("/my-redirect-handler", query_params: {"foo" => "bar"})

response.status.should eq 302
response.headers["Location"].should eq "/redirected"
end
end
end
tip

In the above example we are simply specifying a "raw" path by hardcoding its value. In a real scenario, you will likely want to resolve your handler URLs using the Marten::Routing::Map#reverse method of the main routes map (that way, you don't hardcode route paths in your specs). For example

url = Marten.routes.reverse("article_detail", pk: 42)
response = Marten::Spec.client.get(url, query_params: {"foo" => "bar"})

Here we are simply issuing a GET request (by leveraging the #get test client method) and testing the obtained response. A few things can be noted:

  • The test client does not require your project's server to be running: internally it uses a lightweight server handlers chain that ensures that your project's middlewares are applied and that the URL you requested is resolved and mapped to the right handler
  • Only the path to the handler needs to be specified when issuing requests (eg. /foo/bar)

Note that you can also issue other types of requests by leveraging methods like #post, #put, or #delete. For example:

describe MySchemaHandler do
describe "#post" do
it "validates the data and redirects" do
response = Marten::Spec.client.post("/my-schema-handler", data: {"first_name" => "John", "last_name" => "Doe"})

response.status.should eq 302
response.headers["Location"].should eq "/redirected"
end
end
end
info

By default, CSRF checks are disabled for requests issued by the test client. If for some reasons you need to ensure that those are enabled, you can initialize a Marten::Spec::Client object with disable_request_forgery_protection: false.

Introspecting responses

Responses returned by the test client are instances of the standard Marten::HTTP::Response class. As such you can easily access response attributes such as the status code, the content and content type, cookies, and headers in your specs in order to verify that the expected response was returned by your handler.

Exceptions

It is important to note that exceptions raised in your handlers will be visible from your spec. This means that you should use the standard #expect_raises expectation helper to verify that these exceptions are indeed raised.

Session and cookies

Test clients are always stateful: if a handler sets a cookie in the returned response, then this cookie will be stored in the client's cookie store (available via the #cookies method) and will be automatically sent for subsequent requests issued by the client.

The same goes for session values: such values can be set using the session store returned by the #sessions client method. If you set session values in this store prior to any request, the matched handler will have access to them and the new values that are set by the handler will be available for further inspection once the response is returned. These session values are also maintained between requests issued by a single client.

For example:

describe MyHandler do
describe "#get" do
it "renders the expected content if the right value is in the session" do
Marten::Spec.client.session["foo"] = "bar"

url = Marten.routes.reverse("initiate_request")
response = Marten::Spec.client.get(url)

response.status.should eq 200
response.content.includes?("Initiate request").should be_true
end
end
end

Testing client and authentication

When using the marten-auth shard and the built-in authentication, a few additional helpers can be leveraged in order to easily sign in/sign out users while using the test client:

  • The #sign_in method can be used to simulate the effect of a signed-in user. This means that the user ID will be persisted into the test client session and that requests issued with it will be associated with the considered user
  • The #sign_out method can be used to ensure that any signed-in user is logged out and that the session is flushed

For example:

describe MyHandler do
describe "#get" do
it "shows the profile page of the authenticated user" do
user = Auth::User.create!(email: "[email protected]") do |user
user.set_password("insecure")
end

url = Marten.routes.reverse("auth:profile")

Marten::Spec.client.sign_in(user)
response = Marten::Spec.client.get(url)

response.status.should eq 200
response.content.includes?("Profile").should be_true
end
end
end

Collecting emails

If your code is sending emails, you might want to test that these emails are sent as expected. To do that, you can leverage the development emailing backend to ensure that sent emails are collected as part of each spec execution.

To do that, the emailing backend needs to be initialized with collect_emails: true when configuring the emailing.backend setting. For example:

Marten.configure :test do |config|
config.backend = Marten::Emailing::Backend::Development.new(collect_emails: true)
end

Doing so will ensure that all sent emails are "collected" for further inspection. You can easily retrieve collected emails by calling the Marten::Spec#delivered_emails method, which returns an array of Marten::Email instances. For example:

describe MyObject do
describe "#do_something" do
it "sends an email as expected" do
obj = MyObject.new
obj.do_something

Marten::Spec.delivered_emails.size.should eq 1
Marten::Spec.delivered_emails[0].subject.should eq "Test subject"
end
end
end
info

Note that Marten also automatically ensures that the collected emails are automatically reset after each spec execution so that you don't have to take care of that directly.