At last year's Yow Melbourne developer Conference, I attended some workshops. These workshops are the responsibility of the @coreyhaines and @rains, so TDD (test-driven development) is the main content of the discussion. It's usually not a problem, but it's frustrating (considering this is the 2010 developer conference), when it's not easy to get on the internet, and I just installed my Linux laptop and couldn't download rspec. Luckily, a few weeks ago, I decided to write my own unit test framework (because I had the ability to do this), and then I had a test framework available that solved the problem. However, this reminds me of the question of how much code can be used to write a usable unit-testing framework at least?
One of the smallest available unit tests
The code is very small when you start to write a unit test framework, but it becomes less refined when I want to add some features to it: Fortunately, rewriting is easy. What we really need to do is execute the following code:
Describe ' Some test do
it ' should be true ' do
true.should = = True
end
It ' should show that expression Can be true ' do
(5 = 5). should = = True End
It ' should be failing deliberately ' do
5.should = = 6
end< C11/>end
As you can see, it's much like a basic rspec test. Let's write some code to execute it.
The RSpec tool is a Ruby software package that you can use to build specifications about your software. The specification is actually a test that describes the behavior of the system.
To build a simple framework
The first thing to do is to use "describe" to define a new test. Now that we want to place the "describe" block anywhere (for example, the file itself), we need to make a little extension of Ruby. The "puts" function is in the kernel block, so it can be used anywhere (because the object class contains kernel and every object in Ruby inherits from the object class), and we put describe in the kernel Block to give the same ability):
Module Kernel
def describe (description, &block)
tests = Dsl.new.parse (description, block)
Tests.execute
End
Ruby Block:ruby Language's block function is similar to a callback function.
As you can see, "describe" receives a string to describe the test and a block that contains the test code. Here, we'll explain the test code separately from "describe" (for example, "it" block). So we created the DSL class and used its parse function to process the block to be tested, resulting in an object that could execute all of our tests, but don't get too excited. The DSL class looks like this:
Class DSL
def initialize
@tests = {}
end
def parse (description, block)
Self.instance_eval ( &block)
executor.new (description, @tests)
end
def it (description, &block)
@tests [ Description] = Block
end
The thing to do here is to evaluate the block in the context of the DSL object:
Self.instance_eval (&block)
Our DSL object has an "it" function that also receives a description and a block, which is exactly the same as what the describe block contains, and everything works well (for example, we basically use the "it" function in several function calls, Each time a description and a block are passed in. We can also define other functions in the DSL object, and these functions become part of the language that is allowed to be used in the "describe" block.
In describe block, the "it" function is invoked once for each "it" block. Each time a call is made, the entered block is stored in the hash table as a key value for the test description. Once this is done, we simply create a executor object that iterates through all our test blocks, invokes them, and produces execution results. The executor code is as follows:
Class Executor
def Initialize (description, tests)
@description = description
@tests = Tests
@success_ Count = 0
@failure_count = 0
end
def execute
puts ' #{@description} '
@tests. Each_pair do |name, block|
Print "-#{name}" Result
= Self.instance_eval (&block) result
@success_count + = 1: @failure_count + = 1
puts result? "SUCCESS": "Failure"
end
summary
end
def summary
puts "\n#{@tests. Keys.size} Tests, #{@ Success_count} success, #{@failure_count} failure "End"
Our executor code is very simple. Outputs a description of the "describe" block, and then iterates through all the stored "it" blocks and executes them in the Executor object. There is no particular reason for this, but this means that the Executor object can also contain other functions and can be used as a "language" in the "it" block (for example, part of our DSL can be defined as a function of executor). For example, we can define the following functions on the executor:
def should_be_five (x)
5 = x
End
This function can also be used internally within the "it" block, but this is not necessary for our simple test.
As a result, the "it" block calculates and stores the results, often resulting only in the return value of the last statement of the "it" block (in general Ruby). Here, we want to make sure that the last statement always returns a Boolean value (indicating that the test passed or failed), through which we can output some meaningful hints.
We still have the last step, the "should" function code is as follows:
True.should = = True
5.should = 5
Each object should provide its own "should" function, the following code:
Class Object
def should
self
end
This function does not really work (just return the object itself); it's just a syntax to make the test read better.
At this stage, we simply convert the structure of the test calculation to a string indicating that the test results pass or fail and output. In this process, we will count the number of tests passed or failed, so we can give a summary report at the end. That's all the code we need, and if we put them together, that's the following 44 lines of code:
module Kernel def describe (description, &block) tests = Dsl.new.parse (description , block) Tests.execute End Class Object def should self End class Dsl Def initialize @tests = {} End D EF Parse (description, block) self.instance_eval (&block) executor.new (description, @tests) End def it (description , &block) @tests [description] = Block End Class Executor def initialize (description, tests) @description = d Escription @tests = Tests @success_count = 0 @failure_count = 0 End def execute puts "#{@description}" @tests.
Each_pair do |name, block| Print "-#{name}" result = Self.instance_eval (&block) result? @success_count + 1: @failure_count + = 1 puts result? "SUCCESS": "Failure" End Summary End def summary puts "\n#{@tests. Keys.size} Tests, #{@success_count} SUCCESS, #{@failure_count} failure "End end
If we "need" to use this framework to perform the initial test, we will get the following output:
Some test
-Should be true SUCCESS
-should show, expression can be true SUCCESS
-should be failing deliberately failure
3 Tests, 2 success, 1 failure
That's great! Now, if you're bothered by the lack of a unit test framework and don't want to write code recklessly, it takes 5 minutes to get a test framework that will help you. There are, of course, some slight exaggerations here; you'll soon think of the lack of additional validation APIs, better output, object emulation, Test stubs, and so on. However, we can easily extend some of these features (for example, adding extra DSL elements) to a streamlined framework--with little effort. If you don't believe me, take a look at bacon, which uses only hundreds of lines of code to complete a rspec. The ATTEST test framework that I have written is another good example (so there is a puff suspicion: P). Both of these are missing any built-in test double support, and I'll discuss how to add test double support at another time.
Test Double: The technical term for "Automated unit Testing" in object programming, covering types such as test stub, Mock Object, test Spy, Fake object, and dummy object.