Just wanted to share some of my experiences lately working with Crystal. I’ve been working on a web app the past couple weeks and, even though I’ve been using Crystal for about 6 years now, I’m still blown away by how easy it can be to make code simple to read and write. Writing an integration test via a session-aware layer on top of HotTopic
is pretty slick. A simple one looks like this:
it "gets notifications" do
user = UserFactory.create
notification = NotificationFactory.create(user: user)
session = new_session(user) # No need to stub a whole login process when you can set the active user directly on the session
response = session.get("/notifications")
response.should have_http_status :ok
response.should have_html notification.title # there's also `have_json(foo: "bar")` for API requests
end
Others are more involved and handle interacting with the page similar to Capybara in Ruby (things like session.click_link "Foo"
). I’ll extract that into a shard soon.
The most amazing part of it for me is that, even when my integration tests do multiple requests, run a dozen or more DB queries, load sessions from/save them to Redis, etc, they’re still executing in 2-3ms each on average. The simple one above averages ~1.2ms, even with hundreds of thousands of rows in every table in the test DB (I usually avoid cleaning out test DBs to expose query-performance issues with production-level volumes of data).
This is such a huge departure from my experiences with Ruby on Rails where a single integration test in so many Rails apps I’ve worked on took anywhere from hundreds of milliseconds to multiple seconds each, even when they don’t invoke a headless browser. So to see them taking 2-3ms on average is mind-blowing.
It also reminds me of so many arguments against using factories in Rails app tests or even hitting the DB at all in order to keep your tests from getting too slow, but that’s a perfectly fine thing to do when it all happens in 200 microseconds.
Some of the tools I’m using:
-
armature
for the web framework, sessions, caching API, templates (which are really justECR
but with HTML sanitization), etc -
interro
as the ORM to be able to use a lot of Postgres-specific functionality, uses a similar API to Diesel in Rust - Factories, which are just lightweight wrappers around Interro query objects
- A custom validations mixin to use for mutating query methods, which works really well with Interro’s query style
-
redis
handling backing storage for sessions and caching (viaarmature/redis_session
andarmature/cache
, respectively) -
hot_topic
for testing requests at either the unit-testing or integration-testing layer - A custom Google client for Google OAuth2 and Calendar APIs, stubbed in tests with HotTopic (I would’ve used
PlaceOS/google
, but it uses the pattern where loading the shard loads a lot of APIs I don’t need, leading to increased compilation times) - A custom class on top of
Armature::Session
andHotTopic
to maintain cookies and navigation, currently namedArmature::TestSession
- Some custom
Spec
matchers for things likeresponse.should have_http_status :ok
andresponse.should have_headers HTTP::Headers{"foo" => "bar"}
, which will be extracted along withArmature::TestSession
I’d like to add some tooling to generate some of the code (new routes/queries especially) because it’s currently pretty repetitive, but overall I’m enjoying this style of development.