Testing randomness

Manfred Stienstra, 30 Sep 2008, 10:51 in ruby on rails and testing (edit).

The problem you run into when trying to write unit tests for methods using random data is that you can’t predict what random data it’s going to use. The idea behind a unit test is that you control the input and test whether the output is as you expected, so that’s a problem.

Let’s assume you have a token generator like this:

module Token
  DEFAULT_LENGTH = 8
  CHARACTERS = ('a'..'f').to_a + ('0'..'9').to_a
  
  def self.generate(options={})
    srand
    options[:length] = DEFAULT_LENGTH if options[:length].nil?
    (1..options[:length]).map { CHARACTERS.rand }.join
  end
end

Now you can write tests to see if it generates tokens of the correct length, doesn’t generate homogeneous tokens and doesn’t generate the same token a number of times in a row. Unfortunately these tests could succeed a number of times and suddenly fail for no apparent reason. Usually this happens when you’re on vacation and one of your collegues is running the test on some weird architecture leaving them baffled.

The solution is to make Array#rand predictable by stubbing out the normal implementation:

class Array
  def round_robin_rand_list=(list)
    @round_robin_rand_list = list
  end
  
  def rand
    current = @round_robin_rand_list.shift
    @round_robin_rand_list.push(current)
    current
  end
end

Now you can write a test that checks the output, because you control what the random method returns:

class TokenTest < Test::Unit::TestCase
  def test_token_generate
    Token::CHARACTERS.round_robin_rand_list = [0, 1, 14]
    assert_equal 'ab8ab8ab', Token.generate
    Token::CHARACTERS.round_robin_rand_list = [7, 12, 15, 3]
    assert_equal '169d169d', Token.generate
  end
end

Obviously you need to clean up after yourself in tests so you can’t just redefine methods. It would be really cool if we could do something like this:

class TokenTest < Test::Unit::TestCase
  def test_token_generate
    Token::CHARACTERS.define_round_robin_method(:rand, ['a', 'b', '8'])
    assert_equal 'ab8ab8ab', Token.generate
    Token::CHARACTERS.undefine_round_robin_method(:rand)
  end
end

We’ve implemented this as round_robin_method.rb, complete with its own testsuite. You could go even further and hook it into TestUnit to automatically undefine the round robin methods after a test has run. We’ll leave that as an exercise for another day.

2 comments

ActiveSupport::Multibyte Updated

Manfred Stienstra, 23 Sep 2008, 13:47 in ruby on rails and unicode (edit).

Yesterday Michael Koziarski merged the updated version of ActiveSupport::Multibyte into Rails. The initial reason for the update was Ruby 1.9 compatibility but it turned into a complete overhaul. Not just the code, but also the documentation was revised.

For most people the only noticeable change is the move from String#chars to String#mb_chars. People relying heavily on ActiveSupport::Multibyte probably want to read on.

String#chars renamed to String#mb_chars

One of the initial reasons to use a proxy to access characters back in 2006 was to make Rails future proof in case Ruby got some kind of Unicode support on String. Unfortunately Matz decided to use String#chars for one of these features so we had to change the method name. People running on Ruby <= 1.8.6 will get a nice deprecation warning.

String#mb_chars now returns a proxy on Ruby 1.8 and returns self on Ruby 1.9.

Note that the Ruby 1.9 String class does not implement methods like String#normalize. We’re still trying to figure out how to approach this limitation. For now, you might want to do:

class String
  def normalize(normalization_form=ActiveSupport::Multibyte.default_normalization_form)
    ActiveSupport::Multibyte::Chars.new(self).normalize(normalization_form)
  end
end

No more automatic tidying of bytes

Multibyte no longer attempts to convert broken encoding in strings to a valid UTF-8. The String#tidy_bytes method still exists if you need this functionality.

Duck-typing aid

Strings are notoriously hard to duck-type because they include Enumerable, which makes them hard to differentiate from Arrays. Rails already had some duck-typing help in place for Date, Time and DateTime. We decided to implement the same thing on String and Chars.

'Bambi and Thumper'.acts_like?(:string) #=> true
'Bambi and Thumper'.mb_chars.acts_like?(:string) #=> true

So if you catch yourself using str.is_a?(String) please consider using acts_like?.

Different way of registering backends

Instead of registering a handler on the Chars class, you now set the proxy_class on ActiveSupport::Multibyte.

ActiveSupport::Multibyte.proxy_class = UTF32Chars

Note that this removes a level of indirection, which speeds up the entire Multibyte implementation quite a bit.

If you’ve implemented your own handler, please look at the implementation of ActiveSupport::Multibyte::Chars on how to convert it to work with the new implementation. In most cases this should be a trivial exercise. Don’t hesitate to contact me if you need help.

Overrideable default normalization form

The default normalization form can now be set on ActiveSupport::Multibyte instead of updating a constant.

ActiveSupport::Multibyte.default_normalization_form = :kd

See ActiveSupport::Multibyte::NORMALIZATIONS_FORMS for valid normalization forms.

1 comment

Passenger preference pane v1.1

Eloy Duran, 19 Sep 2008, 18:10 in ruby on rails and tools (edit).

Yes yes, it’s update time!

This version comes with important fixes and some requested improvements. I’ll let the changelog speak for itself:

  • Honor custom environments that a user might have set.
  • Fixed problem with restarting Apache. After saving an application Apache should now automatically be restarted. Thanks to Ciarán Walsh.
  • Added support for ServerAlias and add those entries to the hosts db.
  • Reload the applications from disk when the preference pane is brought back to the front. Any changes made to the vhosts from elsewhere will be reflected in the UI.
  • Moved all hardcoded paths into a config module. Added a config for Apache 2 as installed by MacPorts. Thanks to Ciarán Walsh.
  • The host table list was editable. Thanks to Ciarán Walsh.
  • Fixed bugs in parsing custom user defined data in vhosts.
  • Create a tmp dir before touching restart.txt if none exists.
  • Replace underscores with hyphens in hostnames. Thanks to Bryan Liles.

Most users probably want to download the “stable” 1.1 release.

If you understand why stable has been quoted, you can track development and contribute on: github.com/alloy/passengerpane

Please report any bugs you may find at: fingertips.lighthouseapp.com/projects/13022

9 comments