Speeding up RSpec with bundler standalone and springified binstubs

Testing Rails with RSpec is slow, or at least it feels slow to me in all the projects I am working on. Any speed gain helps my productivity when it reduces the time I'm waiting between writing code and running tests.

There are quite a few posts out there that tackle this problem. But most are pretty old, and none really worked for me, so I'm not going to link to them.

The book I'm reading inspired me to look into the bundler --standalone command. Not that I really understand what's happening there, but at least I got a little bit of a speed bump out of it.

Here's what I did, and how I'm running my tests now.

Spring

First I made sure my Rails app is installed with Spring enabled. Luckily, this is the default. In order to later run RSpec from spring, I added the spring-commands-rspec gem to my Gemfile.

gem 'spring-commands-rspec'

Bundler

Next, I used bundler's standalone command,

$ bundle install --standalone --path .bundle

and then "springified" the installed binstubs.

$ bundle exec spring binstub --all

Problems

I encountered a problem with SQLite 1.4.0. I didn't investigate it further, but pinned the gem to version 1.3 instead.

group :development, :test do
  gem "sqlite3", "~> 1.3.6"
end

Afterwards I repeated the install command.

$ bundle install --standalone --path .bundle

Anytime you want to use bundle install, you now have to use bundle install --standalone instead. I created the bash alias bis for that.

vim-test

I recently started using the vim-test plugin. That plugin has a neat option that makes it use the springified binstubs.

" .vimrc

let test#ruby#use_spring_binstub = 1

Now, when I'm editing app/models/transformer_spec.rb and I hit the return key in normal mode, vim-test executes

$ ./bin/spring rspec spec/models/transformer_spec.rb

Because of spring, everything's faster and I have actually seen tests being executed in less than a second. Still not super fast but better than before.

Teach vim-rails about request specs

Slowly but surely I'm getting to know the really interesting Rails-related Vim plugins. First and foremost: vim-rails. A new feature that I started using recently is the :A alternate file command. Basically, this makes it super quick to jump from a model file to its model spec file. Or from a controller to its spec.

Since Rails 5.?, controller specs have fallen out of DHH's favor. We now use request specs. Unfortunately, vim-rails doesn't know about request specs. Fortunately, we can tell it about them.

vim-rails is very well documented, and :help rails-projections provides the solution to this problem. This is what I put in my .vimrc, and now :A jumps between my controller files and the related request specs.

let g:rails_projections = {
      \ "app/controllers/*_controller.rb": {
      \   "test": [
      \     "spec/controllers/{}_controller_spec.rb",
      \     "spec/requests/{}_spec.rb"
      \   ],
      \ },
      \ "spec/requests/*_spec.rb": {
      \   "alternate": [
      \     "app/controllers/{}_controller.rb",
      \   ],
      \ }}

Running RSpec with a single keystroke in a separate tmux session

This is a tiny update to Running Specs from Vim, Sent to tmux via Tslime from the thoughtbot blog. Go read it, and realize that it's six years old.

vim-rspec has not been updated in two years. It probably still works fine, but there's a new vim plugin that does what vim-rspec does, just better: vim-test.

I finally set this up yesterday:

" Add vim-test and tslime to vim plugins.
Plug 'janko/vim-test'
Plug 'jgdavey/tslime.vim'

...

" Configure vim-test to execute test command using tslime
let test#strategy = "tslime"

" Configure <CR> aka the Return key to run my test file.
nmap <CR> :TestFile<CR>
" I'm still figuring out which test commands make the most sense
" in my workflow. Right now, this feels pretty good.
nmap <leader><CR> :TestLast<CR>

Now, when I'm in normal mode and hit the return key, rspec gets executed for the current file in a different tmux pane (!!). What I didn't understand before using this was how it would select the correct pane. Turns out, it's super easy. On the first run, it asks me for the tmux session, window, and pane (if necessary). After that, it remembers and it always sends the test command there. Super cool!

Unit testing and test parametrization with RSpec?

Last year I made the switch from developing with Python and Django to working with Ruby and Rails. With that, I also had to learn a new test runner. No more pytest :(.

For some reason, a very popular test runner for testing Rails apps is RSpec. Its domain-specific language (DSL) is written with Behavior Driven Development in mind. Hence its tag line:

Behaviour Driven Development for Ruby. Making TDD Productive and Fun.

I'm really struggling with using RSpec, and I wonder why. Most of the time, I'm fighting the framework instead of enjoying its benefits. So I want to explore these questions and hopefully get some answers how to do this better. This is the first post on this topic.

Testing simple functions

Let's take a simple function that adds two numbers.

# Python
def add(a, b):
    return a + b
# Ruby
def add(a, b)
  a + b
end

Using pytest, it takes 2 lines of code to test this function with one set of parameters.

def test_add():
    assert add(5, 4) == 9

Using RSpec, this takes me at least 5 loc.

describe '#add' do
  it 'returns the sum of 2 integers' do
    expect(add(5, 4)).to eq(9)
  end
end

Or 4 if I use the subject shorthand notation.

describe '#add' do
  subject { add(5, 4) }

  it { is_expected.to eq 9 }
end

That's a lot of words for just a simple assertion.

Test parametrization

Now I want to test not only one set of test parameters, but many. With pytest I use the parametrize marker.

import pytest

@pytest.mark.parametrize("a, b, expected", [
    (5, 4, 9),
    (1, 2, 3),
    (1, 2, 4)
])
def test_add(a, b, expected):
    assert add(a, b) == expected

In case of a test failure, pytest shows me exactly which parameters lead to the error.

======== FAILURES =========
________ test_add[1-2-4] ______

a = 1, b = 2, expected = 4

    @pytest.mark.parametrize("a, b, expected", [
        (5, 4, 9),
        (1, 2, 4)
    ])
    def test_add(a, b, expected):
>       assert add(a, b) == expected
E       assert 3 == 4
E        +  where 3 = add(1, 2)

RSpec 1

A naïve Ruby implementation would be to loop over an array of parameter arrays.

params = [
  [5, 4, 9],
  [1, 2, 3],
  [1, 2, 4]
]

describe '#add' do
  params.each do |p|
    it 'returns the sum of 2 integers' do
      expect(add(p[0], p[1])).to eq(p[2])
    end
  end
end

Failure output is not very helpful as I don't see which parameters caused the error.

Failures:

  1) #add returns the sum of 2 integers
     Failure/Error: expect(add(p[0], p[1])).to eq(p[2])

       expected: 4
            got: 3

       (compared using ==)
     # ./demo02_spec.rb:16:in `block (3 levels) in <top (required)>'

RSpec 2

Another approach, that I found on the internets, includes writing a test function and call that function repeatedly using different parameters.

def test_add(a, b, expected)
  describe '#add' do
    subject { add(a, b) }

    it { is_expected.to eq expected }
  end
end

test_add 5, 4, 9
test_add 1, 2, 3
test_add 1, 2, 4

The test function looks a little obscure, but the last part is very readable. Also, when a failure occurs, we can see the exact line where it happens.

Failures:

  1) #add should eq 4
     Failure/Error: it { is_expected.to eq expected }

       expected: 4
            got: 3

       (compared using ==)
     # ./demo03_spec.rb:12:in `block (2 levels) in test_add'

rspec-parametrized

Lastly, there is the rspec-parameterized plugin that provides a syntax that's very close to pytest.

require 'rspec-parameterized'

describe '#add' do
  where(:a, :b, :expected) do
    [
      [4, 5, 9],
      [1, 2, 3],
      [1, 2, 4]
    ]
  end

  with_them do
    it 'returns the sum of 2 integers' do
      expect(add(a, b)).to eq expected
    end
  end
end

Failure output is actually helpful, even if not as clean as pytest:

Failures:

  1) #add a: 1, b: 2, expected: 4 returns the sum of 2 integers
     Failure/Error: expect(add(a, b)).to eq expected

       expected: 4
            got: 3

       (compared using ==)
     # ./demo05_spec.rb:20:in `block (3 levels) in <top (required)>'

The reason we have not yet introduced this plugin into our test suite at work is that the gem has a few dependencies that have not been updated in many years:

  • abstract_type, current version: 0.0.7 (2013)
  • adamantium 0.2.0 (2014)
  • concord 0.1.5 (2014)
  • proc_to_ast 0.1.0 (2015)

Conclusion?

All-in-all, I am still very confused about how to best use RSpec. Testing simple functions includes a lot of boilerplate code that's hard to write and slow to read. Test parameterization is doable but no clear best practice has emerged, yet.

If you can help me out and give me some pointers, or if you can tell me that I'm going at this completely wrong, please mention me on Mastodon (preferred) or Twitter or contact me another way.

Possible future questions include:

  • Property-based testing
  • GIVEN-WHEN-THEN
  • Utilizing --format documentation
  • FactoryBot.create vs. FactoryBot.build

Update August 2018

Working a full-time job in an office is something different. Add 3 hours of commute each day, and my life is a lot different than it was a year ago. I firmly believe that I'm about as productive now than I was when I was working fewer hours but alone in my home office. But that's for another day.

More time to and at work means a lot less time for other things, which is something I am still struggling to get used to. Combine that with having moved across the Atlantic, finding and moving into a new apartment, and dealing with German bureaucracy! All things considered, I guess I'm dealing with it pretty well.

Still, I had to pause a lot of activities during the last four months, including this blog. As I'm learning a new programming language (Ruby) and a new web framework (Rails), there should be enough fun topics to write about (There are). Just like last year with Django and software testing. But who has the time? Not me at the moment. At least I played my first gig with my old band two weeks ago. That was good, and it won't be the last time.

Lastly, in case you're reading this and wonder why, after almost four months back in Germany, I haven't managed to come visit you: I'm sorry, I'm working on it. In the meantime, our guestroom is large, has an even larger bed and is open to you anytime!

Moving back to Germany

My search for a new job has been successful. I'm starting a new position as a Ruby-On-Rails developer with a company at the Frankfurt Airport on May 2nd (I will commute from Mannheim). In the end it took me 4 months, 7 applications, 5 interviews and 2 job offers. This doesn't sound too bad, but still, it's not an experience I want to repeat anytime soon.

Check my Now page to see where I'm currently at in the process of moving back to Germany.

Wait. Rails?

Yupp. Rails.

I'll need to update the about page.

To hear what it feels like to leave Mexico and start something completely new, you'll have to listen to one of the next episodes of Several Ways To Live. There Nick and I will discuss the coming-home part of being an expat.

Let me just quickly give you an incomplete and unordered list of things I am looking forward to in Germany:

  • Döner Kebap
  • my friends
  • my family
  • silence
  • good beer
  • bike tours
  • chaos events
  • my Reggae band
  • clean air
  • forests
  • good bread
  • cross-country skiing
  • drinking beer in public
  • Rindsrouladen
  • affordable good cheese
  • affordable good sausage
  • public swimming pools
  • lactose-free butter

Oh, yes. Döner!!

How to listen to your audio books using your favorite podcast app

This post talks about two things:

  1. how to save backup copies of your Audible audio books
  2. how to create an podcast feed of your audio books

Update 2018-05-27: Audible stopped offering to download the audio files in "Format 4". There are solutions out there, but I haven't gotten around to studying them, yet.

Update 2018-05-29: The solution is on GitHub and is called KrumpetPirate/AAXtoMP3. Thanks to @igami@social.tchncs.de for the tipp!

Why?

I buy all ebooks from the Amazon Kindle store, because I know I can save a backup copy of each book in a non-DRM format. This is not about sharing ebooks but rather about making sure that I can keep a copy of my books in a digital format that I will be able to read in the future, too. Also, I want to be able to use any ebook reading app or device I want to, not just Amazon's Kindle apps. Incidentally, I read most of my ebooks on a Kindle Paperwhite because it's just a great device. I sync the books via the Calibre app, though, and the Kindle device is not registered with my Amazon account.

Anyways.

I want the same for my for Audible books. I love audio books, but I hate that I have to rely on the Audible app, with no protection against someone closing down my Amazon account or what not. Turns out, there is a way to do that, without even removing any DRM, because, apparently, there is no DRM on these files.

How to backup your Audible books

Creating a backup mp3 of audible books is surprisingly simple. Thanks to @octotherp for this tipp!

  1. Go to your Audible library.
  2. In the upper right hand corner, select "Format 4" as the download audio format.
  3. Use ffmpeg to convert the downloaded files to mp3.

To convert a single audio book, the shell command is:

$  ffmpeg -i IAmABook.aa -codec copy IAmABook.mp3

To convert all downloaded audio files, use a shell for loop:

$ for file in *.aa; do ffmpeg -i "$file" -codec copy "${file/%aa/mp3}"; done

This converts all files with the extension .aa and renames them with the extension .mp3.

How to create a podcast feed of your audio books

To create a podcast (RSS) feed from a collection of mp3s is surprisingly easy, too. Because others have solved the problem for us.

I'm using the PHP script DirCaster and my shared hosting account at my favorite web host DreamHost.

Download and unzip DirCaster in a directory

    $ wget http://dircaster.org/DirCasterV09k.zip
    $ mkdir audiobooks
    $ cd audiobooks
    $ unzip ../DirCasterV09k.zip

Edit config_inc.php to set the title and description of the feed. I also add a feed image:

$rssImageUrlTAG   = "https://myaudiobookfeed.com/books.png";

To keep thinks neat, I put the mp3 files into a subfolder books and edit the config_inc.php accordingly:

$mediaDir = "./books"

The audiobooks directory now has to be served by a web server. A simple one that runs PHP is enough though. I put my feed at a random subdomain that no-one is going to find by accident.

At least my favorite podcast app Pocket Casts has no problem subscribing to it. I also tried adding password protection using HTTP auth, but that didn't work.

If you have questions about any of this, please send me a message.

The Xochimilco Trilogy

A few months ago I started a new podcast project Several Ways to Live in Mexico City with my friend Nick. We talk about Mexico and food and culture. And now I think we have created our first highlight of the series, a 3 episode expedition to the town of Xochimilco, where we talked to my friends Jahshua and Edgar.

Episode 8 is a short preview, recorded on the way back from Xochimilco. Episode 9 talks about the history and culture of Xochimilco. And episode 10 starts tackling some questions about -isms that we have observed but could not explain just yet.

  1. SW008 Segundo Piso
  2. SW009 Xochimilco I
  3. SW010 Xochimilco II: Chilling on the Chinampas

If you have comments or questions, please don't hesitate to talk to us on Twitter or Mastodon!

A short work history. Looking for more.

Update 04/04/2018: There's been a development.

My wife Kathrin will not like this post. She will find it self-deprecating and underselling my abilities. But this is my blog, so I don't care.

TL;DR: I am looking for a job as a Django/Python web developer or a test (automation) engineer, working remotely or in the Rhein-Neckar area in Germany. Starting now.

2017

2017 was a pretty good year for me workwise. I spent it working as a Django developer for the local LegalTech startup Lexa.mx. The basic idea of Lexa is that of a Mexican LegalZoom.

While being the sole developer, I also led our product development efforts. These included understanding the structures of Mexican legal agreements, translating the requirements into web forms and making those useful and into a good experience for the users. It was a very interesting inter-disciplinary project that included 3 Mexican lawyers and a designer.

At the same time, this was my first serious Django project. I had to learn a lot. And I did, thanks to mainly the Django documentation, Stack Overflow and the best of them all: ccbv.co.uk.

My favorite part of the Lexa app is how we use Jinja2 templates and LaTeX to dynamically generate the contract PDFs. I've wanted to do this for a long time, ever since a friend and I got the idea for Unkenmathe.

The frontend is ... subject to future improvements. I don't feel comfortable writing CSS because it is so much trial and error for me. For a little bit of dynamic behavior, I sprinkled some VueJS and jQuery on top of everything, mainly the forms, which works nicely. But before I do more JavaScript development I first have to learn how to test that stuff. Developing the backend following the TDD methodology and then testing all the frontend functions manually in the browser feels very wrong.

I also set up and maintained our continuous delivery pipeline using GitHub, CircleCI and PythonAnywhere.

During my time at Lexa I essentially worked remotely. Mexico City is so big that every in-person meeting requires a long commute for everyone involved. But what can I say? I loved it! Working from my quiet home on a schedule that I set myself suits me well, and I've never been more productive.

The main reason why I do not continue working for Lexa is that Kathrin and I will definitely move back to Germany this year, and Lexa is looking to open an office in the city. Remote work that goes beyond the "home office" is pretty much unheard of in Mexico and does not fit with the work culture, which is a shame.

For those not interested in my further work history and how I got to where I am today, here's what else I'm bringing to the table.

2009

A little bit of history.

I left University in early 2009 with a degree in physics, the good-old German "Diplom" (equivalent to a Master of Science). From time to time I am reminded of what I learned in University, and that is a particular way of thinking, ingrained into us students by endless exercises in higher Mathematics. One example is this story of how I finally understood list comprehensions.

But 2009 was not a good year for anybody looking for a job, let alone someone without any real world skills. My only coding skills back then consisted of a little bit of Matlab, numerically solving ordinary differential equations. Sounds amazing, but it's really not. At least I got to experience working as a bike messenger for a few months.

This was also the year when I remembered my 90's experimentations with HTML and started playing around with WordPress.

2011

2011 I got my first "real" job.

Looking for work. Am a physicist. Can do everything.

Yes, this tweet actually worked and landed me a job at a SAP consulting firm, solely based on my degree. It lasted about 18 months before both the company and I were happy to part ways.

Considering that my tasks involved business processes and code, two things I can be very passionate about, it's still a little bit of a mystery why I never got the hang of it, why the work never clicked for me. My best explanation is that I not only lacked mentorship but also there was virtually no relevant documentation that I could study on my own. So much of the company's knowledge was never explicitly written down that it made it too hard even for me to acquire enough of it to succeed in that particular industry.

2013

My favorite job during these years was working for the Ganter brewery where I learned a skill that I am very proud of: tapping the perfect beer. Seriously.

All the while I was developing WordPress sites for clients and administering web hosting accounts. The one service that I am still providing to a number of clients today is securing and backing up their WordPress installations.

2014

2014 I not only met Kathrin, but we also moved to Mexico City. Our first year in Mexico I worked at the German school where Kathrin is teaching. There I finally learned that teaching is not for me. And I didn't even teach. Also, I can't handle saying good morning to 30 people each and every day.

But this way I got to know my friend Angélica with whom I have since collaborated on various web projects using WordPress and Kirby CMS. She also later worked with me on the Lexa project.

2016

I had wanted to learn to program for a long time, but it had never clicked. Reading "programming" books never worked for me.

2016 it finally did click, and it took a book written for non-developers: Hello Web App by Tracy Osborn. It's an introduction to Django for designers and provided me with enough knowledge to later advance to the two other Django books that I cannot recommend enough: Obey the Testing Goat Test-Driven Development with Python by Harry Percival and Two Scoops of Django by Audrey Roy Greenfeld and Daniel Roy Greenfeld.

I've since started working on a couple personal side projects, most notably Unkenmathe and Reggae CDMX which are both still works in progress.

In December of 2016 Angélica and I then sat down with the 3 lawyers that had the idea for Lexa.mx, and that would be my primary occupation for the next 12 months.

What else I'm bringing to the table

I am looking for a job as a Django/Python web developer.

After working on many projects as the sole developer, it's way past time for me to join a team of developers and learn that part of software and web development.

  • I want to learn from others and teach them whenever I can.
  • I can take charge of a project when needed but prefer working among equals.
  • I love to collaborate with experts from non-technical fields – analyzing their problems, and developing and implementing technical solutions.
  • I'm pretty high on the TDD band wagon, and my testing framework of choice is pytest.

As I very much enjoy automated software testing, I am also interested in working as a test engineer. I am well aware that this includes much more than writing automated tests. But it's something that I believe I would enjoy doing, and I definitely know that I can learn it and be good at it.

Timetable

My current plan is to remain in Mexico while Kathrin finishes the school year and for us to move back to the Rhein-Neckar area in Germany in early July. But I am happy to relocate earlier as necessary.

If all this makes you want to think about maybe hiring me, or if you know a friend who might, then let's talk! I also have a detailed CV prepared if that's your sort of thing.

Until then I'll be happily working on Udacity's Full Stack Web Development Nanodegree program to fill some knowledge gaps.

Update 13/01/2018: minor edits.

Speeding up Django unit tests with SQLite, reuse-db and RAMDisk

Recently, Harry wrote a post about how to speed up Django unit-testing by using a persistent in-memory SQLite database.

While Harry uses the default Django testrunner and a Linux machine, I use pytest with pytest-django on a Mac. This changes a few things, and this post will show the differences. So it's very specific to:

  • Django
  • pytest
  • macOS

Create and mount the RAMDisk

/dev/shm is a Linux thing. But Harry already provided the link to a working solution on Stack Overflow to how to do this on macOS.

$ hdiutil attach -nomount ram://$((2 * 1024 * SIZE_IN_MB))
/dev/disk2

$ diskutil eraseVolume HFS+ RAMDisk /dev/disk2

This creates an in-memory Volume that can store the persistent SQLite database.

Use SQLite

Maybe I'm doing this all wrong, but because I use pytest, I configure the test database in a separate configuration file like this.

# config/settings/testing.py
from .common import *

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': ':memory:',
        'TEST': {}
    }
}

Use it in-memory

What --keepdb is for the default testrunner, --reuse-db is for pytest-django. I usually set this as the default for all test runs in pytest.ini.

# pytest.ini
[pytest]
DJANGO_SETTINGS_MODULE=config.settings.testing
addopts = --reuse-db

To not re-use the database, pytest-django's argument is --create-db. So Harry's

if 'keepdb' in sys.argv:

becomes

if not 'create-db' in sys.argv:

in my case. To use the RAMDisk:

# config/settings/testing.py

[...]

import sys
if not 'create-db' in sys.argv:
    # and this allows you to use --reuse-db to skip re-creating the db,
    # even faster!
    DATABASES['default']['TEST']['NAME'] = '/Volumes/RAMDisk/myfunnyproject.test.db.sqlite3'

When there's no RAMDisk

If there is no RAMDisk, for example after a reboot, then the test run fails as soon as it hits the database. Obviously.

>       conn = Database.connect(**conn_params)
E       django.db.utils.OperationalError: unable to open database file

I just throw another if statement in there to check for the existence of the RAMDisk.

if os.path.isdir('/Volumes/RAMDisk') and not 'create-db' in sys.argv:

Summary

You can check a full test configuration here:

https://github.com/FlowFX/unkenmathe.de/blob/master/src/config/settings/testing.py