Development in compilers has advanced so much that static typed languages have reached the same level human-friendly level as dynamic languages and type safety is coming back while dynamic languages are fading away :)
Lately I have been focusing more on safety when I develop code on dynamic languages, especially in Ruby. (I had a couple of days fun in Crystal and it feels like a minefield when working with Ruby again or any other dynamic language)
One of the usual things I do is, in order to avoid code duplication, I use metaprogramming to generate code that I would otherwise write myself. The advantage is two-fold: first it saves me time from writing the same stuff, secondly checks are made by the interpreter when the system boots (or during runtime but I avoid that case).
When running the tests I always like to have in .rspec
the following 2 options:
--color
--format=documentation
Now my usual rspec test file would look like that:
RSpec.describe "Feature: Create account", :type => :feature do
describe "with correct input" do
before :each do
#setup state
end
it "creates an account" do
#run expectations
end
end
describe "with wrong input" do
before :each do
#setup state
end
it "creates an account" do
#run expectations
end
end
Very simple example just for the sake of showing you what I mean.
With those options in .rspec
we would get the following:
Feature: Create account
with correct input
creates an account
with wrong input
gets an error
Do you see a pattern there? Some with
are repeated and also Feature:
will be repeated in every feature.
How can we move that from simple string descriptions to the language itself?
module RSpecWith
def with(text, options = {}, &block)
describe("with #{text}", options, &block)
end
def when(text, options = {}, &block)
describe("when #{text}", options, &block)
end
def inside(text, options = {}, &block)
describe("inside #{text}", options, &block)
end
end
RSpec.configure do |config|
# other stuff...
config.extend RSpecWith
end
Or with some meta alcohol:
module RSpecWith
[:with, :when, :inside].each do |descriptor|
define_method(descriptor) do |text, options = {}, &block|
describe("with #{text}", options, &block)
end
end
end
RSpec.configure do |config|
# other stuff...
config.extend RSpecWith
end
And about the top feature thingy:
RSpec.send(:define_singleton_method, :top_feature) do |text, options, &block|
RSpec.describe("Feature: #{text}", options, &block)
end
So now we basically can write some code instead of strings :)
RSpec.top_feature "Create account", :type => :feature do
with "correct input" do
before :each do
#setup state
end
it "creates an account" do
end
end
with "wrong input" do
before :each do
#setup state
end
it "creates an account" do
#run expectations
end
end
end
I have been using this technique alot through out my code. I explicitly aim for things that can be setup on startup time and not on demand. Creating methods on runtime is really bad idea, I think, but on boot it doesn’t hurt because even if you create, say, 100 new methods and 10 classes it is still OK but imagine if you create 5 methods in each request on Rails and having like 1000 request per minute on a single process.. that would mean 5000 new methods per minute..