One team discovered a jaw-dropping issue with has_many :through. Given the following:
class User < ActiveRecord::Base has_many :user_photos has_many :photos, :through => :user_photosa_user.photos.createwill create and persist both a Photo object and the UserPhoto join objectphoto = a_user.photos.buildfollowed byphoto.savewill create and persist the Photo object only, and will not persist an appropriate UserPhoto join object.
Rails 2.2:
Test::Unit::TestCaseextentions have been removed from Rails Core and are now inActiveSupport::TestCase. As stated in the Groups Thread about this, useActiveSupport::TestCaseinstead ofTest::Unit::TestCasein test/test_helper.rb.
Notes on Google Chrome Compatiblity
Pivot Jonathan and I were recently working on support for Google Chrome in Pivotal Tracker. Tracker’s extensive JsUnit test suite made this a lot easier.
Here’s some quick notes I took on the issues we ran into.
Don’t try to directly mock the ‘reset’ method on a Form Element
This was the original mocking code in one of our JsUnit tests:
var resetCalled = false;
widget._uploadForm.reset = function() { resetCalled = true; };
This permanently blew away the “reset” method, so it was undefined when called in a subsequent. To fix it, we did this in our form builder method:
var element = Element.create("form");
element.nativeReset = element.reset;
element.reset = function() { element.nativeReset() };
Hash keys sort differently
We had a testHash.keys() being compared to a hardcoded array. Chrome sorted the keys differently (apparently non-deterministically, so we had to do an explicit sort:
assertArrayEquals(['10001', '10002', '10003', 'endOfList'].sort(), $H(itemListWidget.draggables).keys().sort());
It wasn’t good to depend on the keys order in the first place, but it worked under IE, Firefox, and Safari.
The same hash sorting bug bit us in a much more obscure way. There was some threading test code that simulated timeouts/concurrency using a mock clock. Previously, the test code was dependent on the order in which the functions were added to a hash the mock “clock”. This broke with a different hash sorting order. We had to simulate some additional “ticks” to make the test pass.
Mozilla, but not Gecko
The browser string returned for Chrome by one of our utility functions, BrowserDetect.browser(), is “Mozilla”. However, for some of our simulated keypress events in tests, the “Gecko” version did not work.
Specifically, we had to use “KeyboardEvent” instead of “KeyEvents”, and “initKeyboardEvent” instead of “initKeyEvent”. See the table in this mozilla doc page.
Here’s the code we used to handle both cases:
evt = document.createEvent('KeyboardEvent');
if (typeof(evt.initKeyboardEvent) != 'undefined') {
evt.initKeyboardEvent(eventName, true, true, window, false, false, false, false, options.keyCode, options.keyCode);
} else {
evt.initKeyEvent(eventName, true, true, window, false, false, false, false, options.keyCode, options.keyCode);
}
The UserAgent (request.user_agent) returns
Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/0.3.154.9 Safari/525.19
The ‘sort’ function does not preserve order of equivalent elements
The following page outputs ‘ACBD’ under Chrome:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<body>
<a href="#" onclick="alert(doSort()); return false;">Sort</a>
<script type="text/javascript">
function doSort() {
var myArray = [
{id: "A", sortVal: 0},
{id: "B", sortVal: 1},
{id: "C", sortVal: 1},
{id: "D", sortVal: 2}
];
var sorted = myArray.sort(function(a,b) {return a.sortVal - b.sortVal});
return sorted[0].id + sorted[1].id + sorted[2].id + sorted[3].id;
}
</script>
</body>
</html>
Standup 11/17/2008: Google Chrome Gotchas
Interesting Things
We recently updated Pivotal Tracker‘s extensive JSUnit test suite to be compatible with the Google Chrome browser. Check out the extensive Notes on Google Chrome Compatiblity post by Pivot Chad.
ActiveScaffold + Rails 2.2 = BOOM. ActiveScaffold will work with Rails 2.1 if you get the version from github. Read about it.
- Rails 2.2 + Rspec 1.11 = FAIL
Why is the sky blue? When does the wind blow? Why do tests fail?
Here’s an interesting mental exercise that recently I’ve found more and more valuable:
- Test drive your code (duh).
- Before each time you run your tests, no matter how small the changes you’ve made, ask yourself why the tests will fail. Don’t just gloss over this; be explicit. Say it out loud or write it down, if that helps. If you have a pair, tell your pair.
You might be surprised how often your assumptions turn out wrong. And, you might be surprised what you learn when you explicitly state those assumptions and then have to justify them when they do turn out wrong.
If you don’t test drive, or you don’t always test drive, (and you’re still reading), ask yourself this: what happens those times when my assumptions are wrong and I don’t have tests to protect me?
Pattern for Functional Testing
Regular Selenium tests (in Java) might look like:
selenium.open("/login");
selenium.type("id=username", "bob");
selenium.type("id=password", "password");
selenium.click("Login");
selenium.waitForPageToLoad();
selenium.click("My Account");
selenium.waitForPageToLoad();
assertEquals("bob", selenium.getText("//table[2]/tr[3]/td[2]/");
After a few tests, this kind of thing becomes painful to manage. The typical solution is to create a bunch of constants for IDs and Xpaths, but that doesn’t help too much.
Fellow Pivot Mike Grafton came up with a cool pattern for improving on this. The idea is to create a class representing each page of your web app. Each class contains two types of methods: a bunch of action methods (clickMyAccountLink(), typeUsername()), and a bunch of inspection commands (isLoginButtonEnabled(), getLoggedInUsername()).
When an action takes you to a new page, the corresponding action method returns a new class representing that page. When it stays on the same page, the method just returns “this”. This allows methods to be chained to make the tests more readable.
Here’s how that test would look using this new pattern:
MyAccountPage myAccountPage = new LoginPage(selenium)
.typeUsername("bob")
.typePassword("password")
.clickLoginButton()
.clickMyAccountLink();
assertEquals("bob", myAccountPage.getLoggedInUsername());
The constructor of each page class should validate that it’s on the correct page (waiting if necessary, and perhaps asserting on the page title).
Standup 11/07/2008: Selenium for Flash
Interesting Things
- Teaser: Selenium for Flash! We’ve developed a Selenium-like framework for Flash. It’s pre-alpha, and needs to be extracted from it’s current home inside a project. Are you interested in a Selenium-like framework for Flash, or have you written one yourself? Let us know!
STI-weirdness. Rails surprise of the day: given a query of a
has_many :photoswhere Photos has STI subclasses (got that?) Rails will build a SQL query that includes the subclass types of Photo, which you might not want:foo.photos.find_by_type("Photo") # query will have "... WHERE type IN ('Photo', 'OriginalPhoto', 'ThumbnailPhoto')"It appears that the retardase_inhibitor might not work with Rails 2.1.X due to fixes in ActionMailer.
- JetBrains has been hard at work: they have released both a new Ruby plugin for IntelliJ, and a ruby-specific IDE (based on IntelliJ) named RubyMine.
Check out Pivot Jonathan’s wife’s art exhibit at Artist-Xchange Gallery in San Francisco, Friday 11/7 from 7-10pm:
Ask for Help
“I want to create a custom launcher for Firefox 2 and Firefox 3 with different profiles. Perhaps the real question is how do we create a custom version of a Mac application launcher, passing in the arguments we need?”
… without having to invoke it on the command line every time.
“We’re trying to delete cookies in our Controller, but they keep appearing in the headers anyway.”
Suggestion: make sure you are specifying your URL paths and domains correctly.
“Why won’t our CSS and other assets load the first time when accessing an SSL-protected domain on Engine Yard?”
It’s most likely not Engine Yard or Firefox 3′s fault. More research needed.
Standup 10/15/2008: this_method; dynamically creating tables for testing
- Where am I? — Ever need to find the name of the method you are currently within? Here’s a
this_methodmethod! The magic is in the REGEX, of course.
module Kernel
private
def this_method
caller[0] =~ /`([^']*)'/ and $1
end
end
- One project wanted to test a very ActiveRecord-specific Module in an isolated, generic way. After spending time researching techniques of mocking and stubbing the many, many ActiveRecord methods that would be touched, they decided to just dynamically create an ActiveRecord and a DB Table for it on the fly! They even used single table inheritance (STI)
describe "MyMagicModule Mixin" do
before(:all) do
ActiveRecord::Base.connection.create_table "some_base_models",
:force => true do |t|
t.string "name"
t.string "type"
t.integer "some_model_b_id", :limit => 11
end
end
after(:all) do
ActiveRecord::Base.connection.drop_table "some_base_models"
end
class SomeBaseModel < ActiveRecord::Base;end
class SomeModelA < SomeBaseModel
include MyMagicModule
belongs_to: :some_model_b
end
class SomeModelB < SomeBaseModel
include MyMagicModule
end
it 'should use special belongs_to stuff from MyMagicModule' do
model_a = SomeModelA.create!(
:name=> "Model A",
:some_model_be => SomeModelB.create!(:name => "Model B"))
# test the functionality from MyMagicModule
end
end
New York Standup 10/2/2008
Selenium on several machines was failing with Connection Refused errors. This turned out to be caused by IPv6 entries (for example, “::1 localhost”) which were added by a recent MacOS upgrade. Commenting out those entries seemed to fix the problem (or work around it, anyway).
Some people expressed a style preference, in rspec, for “pending” rather than an empty “it” block, to make it easy to search for pending tests. Excessive pending tests may be an anti-pattern. On the other hand, writing pending tests, at least temporarily, may be a good way to sketch out an area of functionality before it is implemented.
We have sometimes found that editing selenium tests (but not other ruby files) in IDEA is incredibly slow (as in, 30 second pauses). Two things to try are removing the gems directory from the project (in favor of just those gems which you need to be able to look at in IDEA), or at least removing the selenium gems. Another such example is that IDEA can be really slow editing the end of a long fixture file. This is probably IDEA 7.0.3 or 7.0.4.
Standup 08/27/2008
Does anyone have any experiences with one of the object mother libraries like object daddy? (Answers at standup were “no, we always wrote our own object mothers in a domain-specific way”). The appeal of a library is that it might help keep track of what needs to be done to make an object pass rails validation.
Clock.zone now has exists. (Background, pivotal has a Clock class which has a now method which can be implemented either by a call to Time.now for production, or a mock clock which lets tests specify the “time”). This is so that the rails 2.1 features like Time.zone.now have an analog in Clock.
Standup 7/28/2008
Interesting
- firebug lite 1.2 is out
Ask for Help
We upgraded to rails 2.1 and polonium and our rspecs
are not running on CI, but run fine if you simply use rake on the
command line.
Check to see if you are using the rake extensions in pivotal core bundle.
Using setTimeout() to wait for DOM to update in JsUnit does not work.
Using setTimeout in tests is not going to do what you want, unless you mock setTimeout. Basically, setTimeout kicks off another thread which is not likely to effect the current test.
