Testify

CI Status GitHub release Docs

Testing utilities for Crystal lang specs.

Specs or unit test style?

The both! Based on std's Crystal Spec, Testify is an OOP abstraction for creating unit and integration tests. This allows structuring some tests in an objective of maintenability, extendability and reusability.

Some tests require a Spec style:

it "should create an account and send a welcome email" do
  # ...
end

Other tests require a unitary way:

def test_send_html_email
  # ...
end

def test_send_text_email
  # ...
end

def test_get_with_default_value
  # ...
end

def test_get_without_default_value
  # ...
end

def test_delete_by_id
  # ...
end

Other tests require the benefits of OOP. Advanced example:

# Common tests for all model.
abstract class ModelTest < Testify::Test
  def before_all
    db.connect
    db.create_tables
  end

  def after_all
    db.clean
  end

  def before_each
    db.init
  end

  def after_each
    db.reset
  end

  abstract def db : DBHelper
  abstract def model_class : Model.class
  abstract def get_model_values : Hash
  abstract def get_updated_model_values : Hash

  @[Data("get_model_values")]
  def test_create(values, expected)
    model = model_class.create(values)
    model.should be_a Model
    model.to_h should eq expected
  end

  @[Data("get_updated_model_values")]
  def test_update(values, expected)
    model_class.create(get_model_values)
    model_class.update(values).to_h.should eq expected
    # ...
  end

  def test_delete_by_id
    id = model_class.create(get_model_values)
    id.should be_a(Int32)
    model_class.delete(id).rows_affected.should eq 1
    model_class.find?(id).should eq nil
  end

  def test_find_by_id
    # ...
  end
end

In this example, thanks to ModelTest class defined above. Because we define a common behavior that can be used by all the models that inherit it.

With the main benefits:

Common tests will be automatically executed (by inheritance):

# Test cases for the User model.
class UserTest < ModelTest
  getter db : DBHelper = DBHelper.new
  getter model_class : Model.class = User

  # `Data` source.
  def get_model_values : Hash
    {
      "username" => "foo",
      "email" => "hello@example.org",
      # ...
    }
  end

  # Updated `Data` source.
  def get_updated_model_values : Hash
    user_h = get_model_values
    user_h["username"] = "bar"
    user_h["email"] = "updated@example.org"
    user_h
  end

  # Just write other tests specific to the model User...
end

Installation

  1. Add the dependency to your shard.yml:
   dependencies:
     testify:
       github: nicolab/crystal-testify
       version: ~> 1.0.1 # Check the latest version!
  1. Run shards install

Usage

📘 API doc.


Based and fully compliant with:


Define the test(s) class(es):

require "testify"

class ExampleTest < Testify::Test
  @hey = "Crystal is awesome!"

  def test_something
    true.should eq true
  end

  def test_my_mood
    @hey.should eq "Crystal is awesome!"
  end

  # ...
end

class AnotherTest < Testify::Test
  def test_foo
    true.should eq true
    # ...
  end

  # ...
end

# Runs all test cases
Testify.run_all

A Test class can be run alone:

# Runs only ExampleTest tests
ExampleTest.run

# Runs only AnotherTest tests
AnotherTest.run

Internally, Testify.run_all executes the run method of each Test class defined.

POO

All benefits related to a class are available, like:

Under the hood:

Lifecycle

Optionally if you need life cycle hooks related to your tests.

class ExampleTest < Testify::Test
  def before_all
    puts "before_all"
  end

  def before_each
    puts "before_each"
  end

  def around_all(test)
    puts "around_all - before"
    test.run
    puts "around_all - after"
  end

  def around_each(test)
    puts "around_each - before"
    test.run
    puts "around_each - after"
  end

  def after_all
    puts "before_all"
  end

  def after_each
    puts "before_each"
  end

  # ...
end

Initialize

Optionally if you need to initialize some variables.

class ExampleTest < Testify::Test
  # You can initialize variables, constants, contexts, ...

  def initialize
    # Configure here...
  end
end

Test cases

A test case, it's like:

it "my feature" do
  # ...
end

Except that this is written in the OOP way, in a method:

class ExampleTest < Testify::Test
  # A test case
  def test_my_feature
    # ...
  end

  # Another test.
  def test_another_thing
    # ...
  end

  # ...
end

Pending test / Skip test

Pending test, it's like:

pending "my feature" do
  # ...
end

This can be written:

class ExampleTest < Testify::Test
  # Prefixed by `p`
  def ptest_my_feature
    # ...
  end

  # Pending test with `Pending` annotation.
  @[Pending]
  def test_my_feature
    # ...
  end

  # Pending test with `Skip` annotation.
  # Same as `Pending`, just another syntactic flavor.
  @[Skip]
  def test_my_feature
    # ...
  end

  # ...
end

A class can be skipped:

@[Pending]
# or @[Skip]
class ExampleTest < Testify::Test
  # ...
end

It's like marking a describe block as pending. All tests contained in the current class will be skipped.

Focused test

Focused test, it's like:

it "my feature", focus: true do
  # ...
end

This can be written:

class ExampleTest < Testify::Test
  # Prefixed by `f`
  def ftest_my_feature
  end

  # Focused test with `Focus` annotation.
  @[Focus]
  def test_my_feature
  end
end

A class can be focused:

@[Focus]
class ExampleTest < Testify::Test
  # ...
end

It's like focusing a describe block. Only the tests contained in the focused class will be executed.

Tags

Tags test with Tags annotation, it's like:

it "my feature", tags: "slow" do
  # ...
end

This can be written:

class ExampleTest < Testify::Test
  @[Tags("slow")]
  def test_my_feature
  end
end

A class can be tagged:

@[Tags("foo")]
class ExampleTest < Testify::Test
  # ...
end

It's like tagging a describe block.

Tacker / Tracer

Tracking utilities to trace some behaviors (like a method call, an event listener, a spawn, a Channel, ...).

Development

Install dev dependencies:

shards install

Run:

crystal spec

Clean before commit:

crystal tool format
./bin/ameba

Contributing

  1. Fork it (https://github.com/Nicolab/crystal-testify/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

LICENSE

MIT (c) 2021, Nicolas Talle.

Author

| Nicolas Tallefourtane - Nicolab.net | |---| | Nicolas Talle | | Make a donation via Paypal |

Inspi