Dfect

Assertion testing library for Ruby

Suraj N. Kurapati

21 March 2010

Document




Part 1
Welcome

Dfect is an assertion testing library for Ruby that emphasizes a simple assertion vocabulary, instant debuggability of failures, and flexibility in composing tests.




Resources







1.1  Features

Dfect is exciting because:

  • There are only 5 methods to remember: D F E C T.

  • It lets you debug assertion failures interactively.

  • It keeps a detailed report of assertion failures.

  • It lets you nest tests and execution hooks.

  • Its core consists of a mere 378 lines of code.




1.2  Etymology

Dfect is named after the D F E C T methods it provides.

The name is also play on the word “defect”, whereby the intentional misspelling of “defect” as “dfect” is a defect in itself! ;-)

This wordplay is similar to Mnesia’s play on the word “amnesia”, whereby the intentional omission of the letter “A” indicates forgetfulness—the key characteristic of having amnesia. Clever!




1.3  License

(the ISC license)

Copyright 2009 Suraj N. Kurapati sunaku@gmail.com

Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.




1.4  Credits

Dfect is made possible by contributions from users like you:




Part 2
Setup







2.1  Prerequisites

  • Ruby 1.8.6 or newer.

  • RubyGems 1.3.6 or newer.

  • ruby-debug will be used if it is available. Otherwise the standard interactive Ruby shell (IRB) will be used instead.




2.2  Installing




Command 1.  gem install dfect




2.3  Upgrading




Command 2.  gem update dfect




2.4  Uninstalling




Command 3.  gem uninstall dfect




Part 3
Usage

Begin by loading Dfect into your program:

require 'rubygems' # only necessary if you are using Ruby 1.8
require 'dfect'

You now have access to the Dfect module, which provides methods that can be either mixed-in or called directly, according to your preference:

Dfect.D "hello" do  # D() is a class method
  puts "world"
end

# the above is same as:

include Dfect       # mix-in the Dfect API

D "hello" do        # D() is an instance method
  puts "world"
end






3.1  Assertions

The following methods accept a block parameter and assert something about the result of executing that block. They also accept an optional message, which is shown in failure reports if they fail.

See the API reference for more details and examples.

MethodDescription
Tassert true (not nil and not false)
Fassert not true (nil or false)
Eassert that an execption is raised
Cassert that a symbol is thrown






3.1.1  Negation

These methods are the opposite of normal assertions.

MethodDescription
T!same as F
F!same as T
E!assert that an exception is not raised
C!assert that a symbol is not thrown



3.1.2  Sampling

These methods allow you to check the outcome of an assertion without recording a success or failure for that assertion in the execution report.

MethodDescription
T?returns true if T passes; false otherwise
F?returns true if F passes; false otherwise
E?returns true if E passes; false otherwise
C?returns true if C passes; false otherwise



3.1.3  Failures

When an assertion fails, details about the failure will be shown:

- fail: block must yield true (!nil && !false)
  code: |-
    [12..22] in test/simple.rb
       12
       13     D "with more nested tests" do
       14       x = 5
       15
       16       T { x > 2 }   # passes
    => 17       F { x > 2 }   # fails
       18       E { x.hello } # passes
       19     end
       20   end
       21
       22   # equivalent of before(:each) or setup()
  vars:
    x: 5
    y: 83
  call:
  - test/simple.rb:17
  - test/simple.rb:3

You will then be placed into a debugger to investigate the failure if the :debug option is enabled in the Dfect.options hash.

Details about all assertion failures and a trace of all tests executed are stored by Dfect and provided by the Dfect.report() method.




3.1.4  Emulation

Dfect provides emulation layers for several popular testing libraries:

  • dfect/unit — Test::Unit
  • dfect/mini — Minitest
  • dfect/spec — RSpec

Simply require() one of these emulation layers into your test suite and you can write your tests using the familiar syntax of that testing library. See their source code for more details.




3.2  Tests

The D() method defines a new Dfect test, which is analagous to the concept of test case in xUnit or describe in rSpec. A test may contain nested tests.

D "outer test" do
  # assertions and logic here

  D "inner test" do
    # more assertions and logic here
  end
end






3.2.1  Execution

Tests are executed in depth-first order.

You can configure the test execution process using:

Dfect.options = your_options_hash

You can execute all tests defined thus far using:

Dfect.run

You can stop the execution at any time using:

Dfect.stop

You can view the results of execution using:

puts Dfect.report.to_yaml

See the API reference for details and examples.




Automatic test execution

To mix-in the Dfect module into your program and execute all tests defined by your program before it terminates, simply add the following line at the top of your program:

require 'dfect/auto'






3.2.1.1  Hooks

The D() method provides several entry points (hooks) into the test execution process:

D "outer test" do
  D .<  { puts "before each nested test" }
  D .>  { puts "after  each nested test" }
  D .<< { puts "before all nested tests" }
  D .>> { puts "after  all nested tests" }

  D "inner test" do
    # assertions and logic here
  end
end

A hook method may be called multiple times. Each call registers additional logic to execute during the hook:

D .< { puts "do something" }
D .< { puts "do something more!" }



3.2.1.2  Logging

The L() method lets you insert log messages, composed of arbitrary Ruby objects, into the test execution report.




Example 1.  Logging information in the execution report

When the following test is run:

require 'dfect/auto'

D 'Wizard' do
  L 'Preparing spell to defeat mortal foes...'
end

D 'Magician' do
  L 'Preparing rabbits to pull from hat...', rand(15)
end

D 'Calculator' do
  L Math::PI, [1, 2, 3, ['a', 'b', 'c']], {:foo => 'bar!'}
end

Dfect will output the following:

--- 
- Wizard: 
  - Preparing spell to defeat mortal foes...
- Magician: 
  - Preparing rabbits to pull from hat...
  - 9
- Calculator: 
  - 3.14159265358979
  - - 1
    - 2
    - 3
    - - a
      - b
      - c
  - foo: bar!
--- 
time: 0.000196246



3.2.2  Sharing

The S() method is a mechanism for sharing code. When called with a block, it shares the given block (under a given identifier) for injection into other tests. When called without a block, it injects a previously shared block (under a given identifier) into the environment where it is called.

The S!() method is a combination of the two uses of the S() method: it lets you simultaneously share a block of code while injecting it into the environment where that method is called.

The S?() method simply checks whether any code has been shared under a given identifier.




Example 2.  Sharing code between tests

When the following test is run:

require 'dfect/auto'

S :knowledge do
  L 'Knowledge is power!'
end

D 'Healer' do
  S :knowledge
end

D 'Warrior' do
  S! :strength do
    L 'Strength is power!'
  end
end

D 'Wizard' do
  S :knowledge
  S :strength
end

D 'King' do
  T { S? :knowledge }
  T { S? :strength }
  F { S? :power }
  L 'Power is power!'
end

Dfect will output the following:

--- 
- Healer: 
  - Knowledge is power!
- Warrior: 
  - Strength is power!
- Wizard: 
  - Knowledge is power!
  - Strength is power!
- King: 
  - Power is power!
--- 
pass: 3
time: 0.00045127



3.2.3  Insulation

The D!() method defines a new test that is explicitly insulated from the tests that contain it and also from the top-level Ruby environment. Root-level calls to the D() method are insulated by default.

Inside an insulated test, you are free to:

  • mix-in any modules your test logic needs
  • define your own constants, methods, and classes



Example 3.  Insulated and uninsulated tests

When the following test is run:

require 'dfect/auto'

D "a root-level test" do
  @outside = 1
  T { defined? @outside }
  T { @outside == 1 }

  D "an inner, non-insulated test" do
    T { defined? @outside }
    T { @outside == 1 }
  end

  D! "an inner, insulated test" do
    F { defined? @outside }
    F { @outside == 1 }

    @inside = 2
    T { defined? @inside }
    T { @inside == 2 }
  end

  F { defined? @inside }
  F { @inside == 2 }
end

Dfect will output the following:

--- 
- a root-level test: 
  - an inner, non-insulated test: []

  - an inner, insulated test: []

--- 
pass: 10
time: 0.000505745



Part 4
History







4.1  Version 2.0.0 (2010-03-21)

This release adds the ability to insulate tests from each other, share code between them, makes the order of parameters consistent in the API, improves user interactivity, fixes some bugs, and revises the user manual.




Incompatible changes

  • Root-level calls to the Dfect::D() method are automatically insulated now.

  • The Dfect::E() methods now expects its optional message parameter to be the last parameter in the parameter list.

  • The Dfect::C() methods now expect their first parameter to be a symbol instead of the optional message to be shown in case of assertion failure.

  • The Dfect::R() has been renamed to Dfect::L(), which is a mnemonic for Logging.

  • Shorten names of hash keys in the execution trace for brevity and rename :raise key in report statistics to :error.

  • Only the most helpful subset of the failure details is shown before placing the user into a debugger because they can query the omitted information (on demand) inside the debugger.

  • The execution trace is only shown if all tests passed in Dfect::run().

  • The :debug option is now set to Ruby’s $DEBUG global by default.




New features

  • Print failures as they occur instead of waiting until the end.

  • Allow passing condition as argument to true/false assertions instead of requiring the condition to be passed as a code block, and also fall back to the binding of inner-most enclosing test or hook when debugging or constructing a failure report for an assertion that was not given a block.

    This allows you to reduce “line noise” in your tests:

D "Lottery" do
  winning_ticket = rand()

  D "My chances of winning" do
    my_ticket = rand()
    F my_ticket == winning_ticket, "I won?!  Dream on."
  end
end
  • Add Dfect::S() methods for sharing code between tests.

  • Add Dfect::D!() method to explicitly insulate a test from other tests, the top-level Ruby environment, and the code being tested.

  • Add Dfect::info() method which returns the details of the failure that is currently being debugged by the user.

  • Add instance variables to the :vars section of a failure report.

  • Add setup!() and teardown!() methods for before-all and after-all hooks in the dfect/unit emulation library.

  • Add test execution time to statistics hash (under the :time key).




Bug fixes

  • Do not print any output when :quiet option is active.

  • Allow passing multiple strings/objects to Dfect::D() like in RSpec.

  • Make before and after hook methods mixin-able like assertions.

  • Do not assume that Module#to_s is the same as Module#name.




Housekeeping

  • Upgrade to Inochi 2.0.0 for managing this project.

  • Make emulation libraries modify Dfect module instead of Kernel.

  • Do not pollute the user’s output with our Class#to_yaml workaround.

  • Remove “Motivation” section from user manual. It was too fanatic!




4.2  Version 1.1.0 (2009-10-27)

This release adds a new method for emitting status messages and does some internal housekeeping.




Thank you

  • Iñaki Baz Castillo used Dfect and suggested new features.



New features




Housekeeping

  • Remove unused require of ‘delegate’ standard library in ‘dfect/spec’ RSpec emulation layer.

  • Mention Emulation layers for popular testing libraries.

  • Mention that assertions take an optional message parameter.

  • Replace sample unit test with Dfect test suite.

  • Upgrade user manual to ERBook 9.0.0.




4.3  Version 1.0.1 (2009-10-07)

This release fixes a bug in the Test::Unit emulation library and revises the user manual.




Bug fixes

  • The parameters for the assert_equal() method in the dfect/unit library were in the wrong order.



Housekeeping

  • Revise user manual to better fit jQuery UI tabs.

  • Justify the use of eval() in emulation libraries.

  • Use simpler Copyright reminder at the top of every file.

  • Make SLOC count in user manual reflect the core library only.

  • Mark code spans with {:lang=ruby} instead of HTML <code/> tags.

  • Open source is for fun, so be nice and speak of “related works” instead of “competitors”.




4.4  Version 1.0.0 (2009-05-03)

This release improves default choices, adds emulation layers to mimic other testing libraries, and fixes some bugs.




Incompatible changes

  • The :debug option is now enabled by default and is no longer linked to the value of $DEBUG.

  • Dfect.run() now appends to previous results by default.

    This behavior can be disabled by passing false to the method.




New features

  • Add emulation layers to mimic other testing libraries:

    • dfect/unit — Test::Unit
    • dfect/mini — Minitest
    • dfect/spec — RSpec



Bug fixes

  • Do not blindly replace Class#to_yaml; it might be fixed someday.



Housekeeping

  • Add “Motivation” section in user manual to promote interactive debugging.

  • Add brief History of this project’s inception.

  • Remove redundant assertions for F!() and T!() methods in test suite.

  • Add copyright notice at the top of every file.




4.5  Version 0.1.0 (2009-04-28)

This release adds new variations to assertion methods, fixes several bugs, and improves test coverage.




Thank you

  • François Beausoleil contributed patches for both code and tests! :-)



New features

These new methods implement assertion functionality missing so far (previously we could not assert that a given exception was NOT thrown) and thereby allow us to fully test Dfect using itself.

* Added documentation on how to insulate tests

from the global Ruby namespace.




Bug fixes

  • The E() method did not consider the case where a block does not raise anything as a failure. —François Beausoleil

  • When creating a report about an assertion failure, an exception would be thrown if any local variables pointed to an empty array.

  • The Dfect::<() method broke the inheritance-checking behavior of the < class method.

    Added a bypass to the originial behavior so that RCov::XX can properly generate a report about code that uses Dfect.

  • Added workaround for YAML error when serializing a class object:

    TypeError: can't dump anonymous class Class



Housekeeping

  • Filled the big holes in test coverage. Everything except the runtime debugging logic is now covered by the unit tests.



4.6  Version 0.0.0 (2009-04-13)

For the longest time, I took Test::Unit and RSpec for granted. They were the epitomy of modern Ruby practice; the insurmountable status quo; immortalized in books, conferences, and blogs alike.

Why would anyone think of using anything remotely different, let alone be foolish enough to write an alternative testing library when these are clearly good enough?

Recent experiments in assertion testing libraries smashed my world view:

The status quo was certainly not “good enough”, as I had so blindly believed all these years. In fact, they were verbose behemoths that chose to encode endless permutations of conjecture into methods.

Empowered by this revelation and inspired by Sean O’Halpin’s musing on alternative names for assertion methods, I rose to challenge the status quo.

And so I present to you the first public release of “Dfect”.




This document was generated by ERBook 9.2.1 (2009-11-18) on Sun Mar 21 16:21:47 2010 using the following resources.

Resource Origin License
here_frag command important warning caution note tip quote nav_here nav_prev nav_next nav_list Tango Icon Theme

© 2005 Tango Desktop Project

Creative Commons Attribution-ShareAlike 2.5 License Agreement
hyperlink MediaWiki Monobook Skin

© 2007 MediaWiki contributors

GNU General Public License, version 2

Valid XHTML 1.0 Strict Valid CSS 3!