AsciiMathML

Wednesday, June 17, 2009

Preventing tests (or, 5 Whys in action)

I was working on some Clojure for my site, Reasonr today. I develop on my laptop, using Emacs, and I have a production box on AWS. I recently set up swank-clojure on the production web server, so I can SSH tunnel onto the box, and get a REPL. Very cool.

There is one downside however. I had tunneled into the box to check something out, and then forgot about. I went back to developing. I was ready to test my changes, and I ran (run-all-tests). Normally this would be fine, except my slime buffer was still connected to the production box, and run-all-tests does more than just unit tests, it does things like insert (fake) data into the database and makes sure it comes back out correctly. And I was still connected to the production box, so now that fake data is in my production DB.

#$^%#

After deleting all the crap data out of the DB, I thought of Eric Ries' 5 Whys, and set about trying to make sure this never happens again.

Step 1: Don't allow tests in production



(defn no-tests-for-you []
(println "You're in production, dumbass. No tests for you!"))

(defn prevent-tests []
(with-ns/with-ns 'clojure.contrib.test-is)
(def run-all-tests user/no-tests-for-you)
(def test-ns user/no-tests-for-you)
(def test-var user/no-tests-for-you))

(when (= winston.env/environment :production)
(prevent-tests))


Here, I have a dummy function that reminds the user we're talking to the production box. I also have a function that redefines most of the ways to run clojure tests. with-ns is from clojure.contrib.with-ns, which evaluates body in the specified namespace. Thankfully, (run-all-tests) is the most common way for me to start tests, and the remaining ways all take more effort to run, so this net will likely catch most of my goofs. I also have a var, winston.env/environment which specifies if we're in production or development. If we're in production, we obviously don't want to run tests.

Step 2: Fix the prompt



A big part of my confusion was because I wasn't sure which box I was connected to. Let's fix that by modifying the repl prompt to print the hostname. I already had code that started a repl manually, rather than using the default one in clojure.main. All I had to do was supply a new definition for the repl-prompt

(def hostname (.trim (sh "hostname")))

(defn repl-prompt []
(printf "%s:%s=> " winston.env/hostname *ns*))

(defn start-repl []
(binding [clojure.main/repl-prompt winston.repl/repl-prompt]
(clojure.main/repl)))



I used the sh function from clojure.contrib.shell-out to determine the hostname, and store that in a variable. Then I modified the repl prompt to print hostname:namespace=> rather than just "namespace=>". binding is clojure's way to safely monkey patch. Inside the scope of binding, all calls to clojure.main/repl-prompt will be replaced with calls to winston.repl/repl-prompt.

Step 3: Fix Slime?



I sort of cheated on step 2. Everything I wrote works great, but it doesn't fix Slime. Apparently slime hardcodes the definition of the prompt. You'll see the modified prompt when using the repl at the console, but not via slime. And I don't know of a good way to fix the slime prompt. Anyone out there have ideas?

9 comments:

Gojomo said...

FYI: reasonr.com (as linked) doesn't resolve for me; only www.reasonr.com does.

Asim said...

I prefer to just not write tests that hit the database. Just write small classes and then unit test them and then knit them together to form a whole system. If the tests don't hit the database there is no risk of inserting garbage or dropping tables.

Allen Rohner said...

Asim: yes, you can write tests that don't hit the DB, but then how do you know that code works?

I agree you should avoid it, but I still want to know I have a working app.

Matt Brubeck said...

I like to write tests so that database access happens inside a transaction that is rolled back after the test completes. This also allows each test to run in isolation, so I don't have any hidden dependencies between tests.

Allen Rohner said...

Matt Brubeck: That too is great where applicable, but I also have functional tests using webdriver that visit the UI, insert data into forms, and then assert the data shows up on other pages. Rolling back the transaction obviously doesn't work in those cases.

Groby said...

The easy way to avoid this is having a separate DB that's used by the tests.

Nothing wrong with your changes, either - it just seems a name change would've been less work ;)

Tattoo said...

I think you should always write test cases that clean themselves up after the test case. This is why most unit test tools have, I think, setUp- and tearDown-methods (or something similar)

Mike Stone said...

@Groby: That's the way Rails works, which is my platform of choice at the moment. They go a step further and have a separate database name for development, production and test, further reducing risk that you clobber your production environment during development.

Greg said...

We set things up so that the ssh tunnel settings to the database connect you to a read-only account on production. Safe, and fun.