assert{ 2.0 }

by Phlip

I like developer tests, but I don't like the primitive assertions - assert_equal, assert_match, assert_not_nil, etc. They only exist for one reason - to print out their input values when they fail. And they don't even reflect their variable names.

So I wrote an assertion to replace all of them. Put whatever you want into it; it prints out your expression, and all its values. Essentially like this:

__source__ __failure_diagnostic__
x = 43
assert{ x == 42 }
assert{ x == 42 } --> false - should pass
    x --> 43
deny{ x == 43 }
deny{ x == 43 } --> true - should not pass
    x --> 43

The classic versions require a lot more typing, and reflect much less information:

  assert_equal(x, 42)     --> <43> expected but was \n<42> 
  assert_not_equal(x, 43) --> <43> expected to be != to \n<43>

Install this system with:

  gem install assert2

Some systems might require sudo, to tell the 'puter who's boss. The "assert2" gem will pull in RubyNode, the library that inspects Ruby blocks. Then add require 'assert2' to your test suites, or to your test_helper.rb file.


28 Comments

Jarkko Laine
2008-02-12 00:58:25
    assert_equal(x, 42)     --> <43> expected but was \n<42> 
assert_not_equal(x, 43) --> <43> expected to be != to \n<43>


Dunno if it was intentional or not, but you made the classical mistake of having the arguments backwards in the assertions. You don't expect 42 to be x, you expect x to be 42.


The fact that assert_equal and friends have the arguments in so counterintuitive order was one of the reasons that drove me to BDD and RSpec. And I couldn't be happier :-)

Ovid
2008-02-12 01:15:35
@Jarkko: no offense, but I have serious doubts about the wisdom of RSpec. Others just think it's a terrible idea.


You're spot on about the arguments being reversed, though :)

chromatic
2008-02-12 03:06:59
@Ovid, I'm not sure that RSpec is a terrible idea as such, but its implementation (at least at the syntax level) has some bogosity. I still think that the Perl test style has the greatest advantages over the default xUnit style.
Phlip
2008-02-12 06:03:10
you made the classical mistake of having the arguments backwards in the assertions


Two mistakes - one mine and one belonging to assert_equal.


All my classic assert_equalses, in every project I ever wrote, go assert_equal(reference, sample). The above is the only time I ever wrote (sample, reference). I did it as a lexical technique, to obey "use parallel construction on parallel concepts".


The other mistake is assert_equal's own verbiage. If you must invent a new assert_equal (if, for example, your library does not support the reflection that assert{ 2.0 } requires), then you should make your verbiage as neutral as possible. "<43> should equal <42>". That way the argument order should not matter - even if all your test cases use a standard order!




the Perl test style


Oh, guys, please feel free to have an unrelated language shoot-out, here, so my blog entry will go to the top of "The Hot 25"! Thanks!

Mark Thomas
2008-02-12 06:16:06
@Ovid: The link you reference is full of incorrect assumptions about RSpec. Source filtering? Perhaps you'd have to do that in Perl to achieve that kind of syntax, but Rspec is valid Ruby. And you don't have to define "should" and "should_not" methods. RSpec adds them automatically to every Object, something you cannot do in Perl without a lot of AUTOLOAD hackery. RSpec is more elegant than he gives it credit for, because it takes advantage of these Rubyisms.
Rob Sanheim
2008-02-12 06:58:11
This looks very cool! I'm a bdd guy myself but will take a look at using this w/ my test/spec helpers, as it looks so much more developer friendly. Nice work.
Ovid
2008-02-12 08:19:10
@Mark Thomas: you wrote RSpec adds [methods] automatically to every Object, something you cannot do in Perl without a lot of AUTOLOAD hackery.


I'm not sure where you get this idea. It's trivial in Perl to add new methods to classes on the fly (no AUTOLOAD required).


*Some::Class::new_method = sub { ... };


My objection is not about source filtering. It's about adding new methods to these classes. While it's a cool idea, I don't think it's necessary or warranted. When I read tests, I want to see the behavior of the code, but adding new behavior to classes solely for the purposes of test suites seems a bit dodgy.


That being said, maybe it will turn out to be a non-issue. There's a big difference between me thinking about how code should be and actually trying it :)

Mark Thomas
2008-02-12 12:35:46
@Ovid:


Your example only adds a method to *one* hardcoded class. RSpec adds a method to Object, which is inherited by *all* classes.


but adding new behavior to classes solely for the purposes of test suites seems a bit dodgy


Hee hee... Ruby's open classes make it easy to do and it is used to great effect in many places (including Rails)

John
2008-02-12 13:01:05
@Mark Thomas wrote: "Your example only adds a method to *one* hardcoded class."


sub UNIVERSAL::new_method { ... }


That sort of thing is frowned upon in Perl circles, but that doesn't mean it's not trivial to do.

scott
2008-02-13 17:10:02
Why even bother with deny{ x == 43 } ? Why not just assert{ x != 43 } ?
Phlip
2008-02-13 19:24:53
deny{} is for situations like assert_nil().


In terms of style, all programming statements should be positive. Sometimes assert{ x != 42 } is clear and expressive, and sometimes it isn't. assert{ object.nil? } might not be. deny{ object } might be.


(The source code to deny{} quote the Black Knight from "Monty Python and the Holy Grail" - "None shall pass!")

Phlip
2008-02-13 20:49:37
RSpec


assert2 0.2.0 might work with RSpec, like this:


assert do
my_object.my_method.should eql(42)
end


(Or whatever the syntax is. Yes I have worked with RSpec before...)


The .should should raise an exception, and assert will decorate it with the assertion reflections, and raise it again.

JEG2
2008-02-14 07:10:02
You can customize the messages for the old assertions:


assert_not_nil(topics[topic], "topics[topic] expected not to be nil")


Of course, that's not DRY. To me thought, that's just a great hint that we could do a lot better fine tuning the message, which shouldn't really be code centric anyway:


assert_not_nil(topics[topic], "Your topic (#{topic}) was not found in the list of topics")


Gregory Brown
2008-02-14 07:49:44
I may be missing something, but I'm not sure that assert { 2.0 } offers much different over Test::Unit's assert_block().


This method is what's used to implement all of Test::Unit's custom assertions.

Phlip
2008-02-14 13:32:03
assert_block does not reflect its variables' values when it flunks.


And few custom assertions use assert_block. That's just wishful thinking in assert_block's documentation. (There are also plenty of reasons not to use it, including it will waste time formatting an error message, each time the assertion passes.)

Steve
2008-02-14 13:35:29
Honestly, even if assert_block has the same functionality as assert 2.0, or even if a thousand other hacks and work arounds achieved the same overall result, the main idea here is that assert 2.0 makes this stuff *easy*...why hack when you can do things cleanly?
Steve
2008-02-14 13:41:27
You can customize the messages for the old assertions


You can, but again, its down to convenience...why should I waste time trying to invent a meaningful error message when the most meaningful thing I could get back would be the expression I wrote and a clear reason why it failed?

Phlip
2008-02-14 13:52:54
One theme of misunderstanding here. assert{ 2.0 } is not...


- a DSL like RSpec
- a replacement for "custom assertions"


It lets you write whatever DSL works for you inside the {}. And it should not compete with domain-specific assertions, or application-specific assertion. They should report specific details when they fail, not complex generalities.


assert{ 2.0 } replaces all the primitive RubyUnit assertions except the block-oriented ones, like assert_raise, and the domain-specific ones, like assert_in_delta. That is specific to the domain of floating point numbers.

Kevin Teague
2008-02-14 21:45:15
assert{ 2.0 } looks pretty similar to doctesting, except with doctesting you can just grab an interactive session and paste it into a file and it becomes a test suite.


>> topics = create_topics
>> topics['first']
=> 'a topic'
>> topics['second']
=> nil
>> topics['third'].index('substring')
5


Glenn
2008-02-15 02:38:04
In the interests of keeping the language/DSL war going *groan*...


@Ovid: I've been using RSpec on numerous projects for quite a while now and while there are times when it doesn't behave exactly as expected, and delving into the internals to see what has really happened can be troublesome, I thought I'd pick up this point:


"When I read tests, I want to see the behavior of the code, but adding new behavior to classes solely for the purposes of test suites seems a bit dodgy."


When I read tests, I want to see the intention of the code. RSpec lets me sit down with the client, detail the business requirements in language they understand, and then implement the tests in the same language. I can then setup continuous integration to generate a specdoc report so that they can monitor progress on demand and see what core bits of functionality are working, and what I know isn't. It keeps them in the feedback loop without disturbing me, and gives them a point of reference so they can differentiate between "this isn't working" and "this isn't working as I expected".


The only time I want to see the behaviour of the code in this scenario is when things aren't working, and if the RSpec DSL doesn't give me the language or output I desire then it's trivial to write my own custom matcher.


As for the merits of assert{ 2.0 } it obviously depends on your desire. It doesn't suit me as it's not code that could be read and understood by someone that doesn't understand ruby and removes legibility in the interests of improving debugging when there is an error. I use tests as much for documentation as I do error trapping.

Phlip
2008-02-15 07:06:23
You can customize the messages for the old assertions:


  assert_not_nil(topics[topic], "topics[topic] expected not to be nil")


That's not very DRY. It repeats...



  • "assert" and "expected"

  • not nil

  • "topics[topic]"


To improve that diagnostic message, you'd also have to reflect the contents of topics, and the value found at topics[topic]. And that wouldn't be DRY, either.


After improving that diagnostic, you must find every other primitive assert_*() in your program. You must upgrade each and every one of their diagnostic messages, to achieve that level of detail. You must do all of them, because you don't know which one will bite you at code maintenance time.


And all of those diagnostic messages would not be DRY. That means you get all the common problems with duplicated code. The code might change, leaving the diagnostic messages behind. Then, when they fail, instead of saying "that should not be nil!", they might say something misleading.


Diagnostic messages are like comments - they can lie more easily than code can!

Phlip
2008-02-15 09:42:34
The only time I want to see the behaviour of the code in this scenario is when things aren't working, and if the RSpec DSL doesn't give me the language or output I desire then it's trivial to write my own custom matcher.


In the TestFoodPyramid , the peak is QA tests, soak tests, integration tests, permutation tests, etc.



Down in the middle are the customer-facing tests, acceptance tests, functional tests, etc. You are describing that layer. No assertion should be primitive!


At the bottom layer, every line of code should have matching, primitive tests. These must be dirt-simple to write. assert_equal was invented to provide these conveniences. Your comments also apply to assert_equal. Of _course_ you should productize and self-document the higher-level tests.


You can't DSL the bottom layer of that pyramid, very simply because unit test code should run as close as possible to the tested code. Any layer of abstraction adds noise to the signal from the raw tests...

Bruce
2008-02-17 17:46:15
The problem with RSpec, in my rose-colored syntax world, is it's the fact it's a huge collection of DSL anti-patterns. At any given moment you wonder, "Do I use a period, and underscore, or a space to separate these words?" I'm all for "fluent" syntax, but I think there's a line it crosses (just like AppleScript does), and it's sad, given the smart people involved in the project and some other very cool things I think RSpec is doing right.


Another gripe: like being able to scan the left side of a chunk of tests and see what's expected all in one place; RSpec makes me read across every line (since it feels the inexplicable need to be a sentence), which just slows me down.


I wish the community would have jumped on a lighter weight BDD framework; thankfully it's losing some weight (mocking) and has lost some of it's NIH syndrome oddities (ie, inexplicably writing a new runner vs sitting on top of Test::Unit). I won't hold my breath on the syntax (whereas spacing out an 'assert' and the curly brace is easy to do for this library ;-)


Will be nice to see if this works with test/spec.

Phlip
2008-02-17 22:51:53
The problem with RSpec is it has very little to do with my assertion, I did not target it, I was not even thinking about it, and it is welcome to its niche. I also did not mention it in my original post, but that hasn't stopped everyone from replying as if I did!


That said, I just now spent this evening upgrading a Beast test case into a spec ... case. Thing. Here's the result:



it 'should require body for post' do
post.valid?
post.errors.on(:body).should match(/can't be blank/)
end

We may eventually install the RubyReflector inside RSpec, so it actually help provide all those "well-formed English sentences" at fault time. However, RSpec's usefulness is when it passes, and emits a client-readable list of everything they are getting. Errors are developer-facing.


To demonstrate this, I put assert{} inside it{}, inserted a fault, and pulled the rip-cord:


  it 'should require body for post' do
assert do
@post.valid?
@post.errors.on(:body).should match(/can't be blonk/)
end
end


I got an unholy blast of stack traces, object inspections, and (yes) reflected source with its values. Here's an excerpt:


  Test::Unit::AssertionFailedError in 'Post should require body for post'
#
/home/phlip/projects/beast/stable-1.0/vendor/plugins/rspec/lib/spec/expectations.rb:52:in `fail_with'
...
/home/phlip/projects/beast/stable-1.0/vendor/plugins/rspec/lib/spec/expectations/handler.rb:21:in `handle_matcher'
/home/phlip/projects/beast/stable-1.0/vendor/plugins/rspec/lib/spec/runner/command_line.rb:19:in `run'
/home/phlip/projects/beast/stable-1.0/vendor/plugins/rspec/bin/spec:4

assert{ @post.valid?()
@post.errors().on(:body).should(match(/can't be blonk/))
} --> nil - should pass
@post --> #["can't be bla***
@post.valid?()
--> false
@post.errors() --> #["can't be blank"], "user_id"=>["can't b***
@post.errors().on(:body) --> "can't be blank"
match(/can't be blonk/) --> #
@post.errors().on(:body).should(match(/can't be blonk/))
m--? expected "can't be blank" to match /can't be blonk/.
./spec/models/post_spec.rb:12:


That's obviously pure heaven for a Real Programmer, but don't show that to a civilian client unless you are very good at CPR!


Ever since inventing assert{ 2.0 }, my career path has not lead me to write any new modules for anything with it! When that happens, I will be better prepared to demonstrate how to write DRY code inside its block that reflects error messages cleanly. And my RubyReflector is also available for other assertion systems to use, too...

Knol
2008-02-20 14:11:08
very nice code construction
Phlip
2008-02-25 12:57:08
Consider this assertion:


  assert complex_thing


Under any classical assertion system, this upgrade would be unwise:


  assert complex_thing and another_complex_thing


You must put the other complex thing into its own disjoint assertion.


The assert{ 2.0 } fix:


  assert{ complex_thing and another_complex_thing }


That's less risky. There's other reasons not to make assertions too complex, but "bad diagnosis at fault time" is no longer one of them. If the assertion fails, the diagnostic will state which branch of the and failed.

cheap mp3 player
2008-06-09 09:54:05
访问量最高的cheap mp3 player网站,提供最新cheap mp3 player信息,山东cheap mp3 player,青岛cheap mp3 player,cheap mp3 player指南.cheap mp3 player公司-北京金橄榄cheap mp3 player公司是首都信誉卓著的cheap mp3 player公司,本cheap mp3 player公司提供50多个语种的cheap mp3 player服务.cheap mp3 player第一条 为了规范cheap mp3 player活动,促进cheap mp3 player业的健康发展,保护cheap mp3 player消费者的合法权益,桂林cheap mp3 player网--主要提供桂林cheap mp3 player,桂林酒店预定,桂林cheap mp3 player景点;桂林自助cheap mp3 player,桂林自驾车cheap mp3 player;cheap mp3 player车租赁服务6787673@WOWGOLDS.COM
cheap world of warcraft account
2008-06-09 14:13:45
提供cheap world of warcraft account设计,画册cheap world of warcraft account作品,中国cheap world of warcraft account设计网,一个寻找cheap world of warcraft account灵感的地方!提供最有价值的cheap world of warcraft account设计专业cheap world of warcraft account贸易市场是全球顶尖的cheap world of warcraft account产品交易市场,海量精选的cheap world of warcraft account产品供应信息,cheap world of warcraft account公司黄页中国cheap world of warcraft account人网是综合性的cheap world of warcraft account行业网站,包括对cheap world of warcraft account媒介、cheap world of warcraft account创意、cheap world of warcraft account市场等cheap world of warcraft account文章和cheap world of warcraft account讨论区全思cheap world of warcraft account下设北京cheap world of warcraft account公司|广州cheap world of warcraft account公司|上海cheap world of warcraft account公司|天津cheap world of warcraft account公司|重庆cheap world of warcraft account公司|沈阳cheap world of warcraft account公司6787673@WOWGOLDS.COM