https://github.com/nicolab/crystal-testify
Testing utilities for Crystal lang specs. OOP abstraction for creating unit and integration tests.
https://github.com/nicolab/crystal-testify
crystal crystal-lang integration integration-testing spec test unit-testing xunit
Last synced: about 2 months ago
JSON representation
Testing utilities for Crystal lang specs. OOP abstraction for creating unit and integration tests.
- Host: GitHub
- URL: https://github.com/nicolab/crystal-testify
- Owner: Nicolab
- License: other
- Created: 2021-03-20T19:05:03.000Z (about 4 years ago)
- Default Branch: master
- Last Pushed: 2021-03-26T19:19:36.000Z (about 4 years ago)
- Last Synced: 2025-01-30T05:15:25.577Z (4 months ago)
- Topics: crystal, crystal-lang, integration, integration-testing, spec, test, unit-testing, xunit
- Language: Crystal
- Homepage: https://nicolab.github.io/crystal-testify/
- Size: 60.5 KB
- Stars: 2
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
- License: LICENSE
Awesome Lists containing this project
README
# Testify
[](https://github.com/Nicolab/crystal-testify/actions) [](https://github.com/Nicolab/crystal-testify/releases) [](https://nicolab.github.io/crystal-testify/)
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:
```crystal
it "should create an account and send a welcome email" do
# ...
end
```Other tests require a unitary way:
```crystal
def test_send_html_email
# ...
enddef test_send_text_email
# ...
enddef test_get_with_default_value
# ...
enddef test_get_without_default_value
# ...
enddef test_delete_by_id
# ...
end
```Other tests require the benefits of OOP. Advanced example:
```crystal
# Common tests for all model.
abstract class ModelTest < Testify::Test
def before_all
db.connect
db.create_tables
enddef after_all
db.clean
enddef before_each
db.init
enddef after_each
db.reset
endabstract 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
# ...
enddef 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
enddef 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 lifecycle hooks (before_all, before_each, ...).
* Some tests common to all models do not need to be repeated.
* Clean structured fashion for all models.
* Reusability (example: `AdminTest < UserTest` that reuses common tests, states and `Data` source).Common tests will be automatically executed (by inheritance):
```crystal
# 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" => "[email protected]",
# ...
}
end# Updated `Data` source.
def get_updated_model_values : Hash
user_h = get_model_values
user_h["username"] = "bar"
user_h["email"] = "[email protected]"
user_h
end# Just write other tests specific to the model User...
end
```## Installation
1. Add the dependency to your `shard.yml`:
```yaml
dependencies:
testify:
github: nicolab/crystal-testify
version: ~> 1.0.1 # Check the latest version!
```2. Run `shards install`
## Usage
📘 [API doc](https://nicolab.github.io/crystal-testify/).
---
Based and fully compliant with:
* [Crystal Spec std's](https://crystal-lang.org/api/Spec.html)
* [Testing Crystal Code](https://crystal-lang.org/reference/guides/testing.html)---
Define the test(s) class(es):
```crystal
require "testify"class ExampleTest < Testify::Test
@hey = "Crystal is awesome!"def test_something
true.should eq true
enddef test_my_mood
@hey.should eq "Crystal is awesome!"
end# ...
endclass 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:
```crystal
# 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:
* [macros: hooks](https://crystal-lang.org/reference/syntax_and_semantics/macros/hooks.html)
* [finalize](https://crystal-lang.org/reference/syntax_and_semantics/finalize.html)
* [annotations](https://crystal-lang.org/reference/syntax_and_semantics/annotations/index.html)
* Macros, inheritance, modules, variables, methods, visibility, ... powerful 🚀Under the hood:
* A class defines a "describe" block.
* Methods `test_` and `ftest_`, define a `it` block.
* `ptest_` method, `Pending` and `Skip` annotations, define a `pending` block.
* `ftest_` method and `Focus` annotation, add `focus: true` to a `it` block.
* `Tags("foo")` annotation on a `test` method, add `tags: "foo"` to a `it` block.
* `Tags("foo")` annotation on a `Test` class, add `tags: "foo"` to a `describe` block.### Lifecycle
Optionally if you need life cycle hooks related to your tests.
```crystal
class ExampleTest < Testify::Test
def before_all
puts "before_all"
enddef before_each
puts "before_each"
enddef around_all(test)
puts "around_all - before"
test.run
puts "around_all - after"
enddef around_each(test)
puts "around_each - before"
test.run
puts "around_each - after"
enddef after_all
puts "before_all"
enddef after_each
puts "before_each"
end# ...
end
```### Initialize
Optionally if you need to initialize some variables.
```crystal
class ExampleTest < Testify::Test
# You can initialize variables, constants, contexts, ...def initialize
# Configure here...
end
end
```### Test cases
A test case, it's like:
```crystal
it "my feature" do
# ...
end
```Except that this is written in the OOP way, in a method:
```crystal
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:
```crystal
pending "my feature" do
# ...
end
```This can be written:
```crystal
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:
```crystal
@[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:
```crystal
it "my feature", focus: true do
# ...
end
```This can be written:
```crystal
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:
```crystal
@[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:
```crystal
it "my feature", tags: "slow" do
# ...
end
```This can be written:
```crystal
class ExampleTest < Testify::Test
@[Tags("slow")]
def test_my_feature
end
end
```A class can be tagged:
```crystal
@[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:
```sh
shards install
```Run:
```sh
crystal spec
```Clean before commit:
```sh
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](https://github.com/Nicolab/crystal-testify/blob/master/LICENSE) (c) 2021, Nicolas Talle.
## Author
| [](https://github.com/sponsors/Nicolab) |
|---|
| [Nicolas Talle](https://github.com/sponsors/Nicolab) |
| [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=PGRH4ZXP36GUC) |### Inspi
* [Unit testing](https://en.wikipedia.org/wiki/Unit_testing)
* [Integration testing](https://en.wikipedia.org/wiki/Integration_testing)
* [Data-Driven testing](https://en.wikipedia.org/wiki/Data-driven_testing)
* [ASPEC class](https://github.com/athena-framework/spec/blob/a28a66ee0985d5aed7948183a5942c1c04848a31/src/test_case.cr)
* [Testing Crystal Code](https://crystal-lang.org/reference/guides/testing.html)
* [Crystal Spec std's](https://crystal-lang.org/api/Spec.html)