Class: ZenTest
Overview
ZenTest scans your target and unit-test code and writes your missing code based on simple naming rules, enabling XP at a much quicker pace. ZenTest only works with Ruby and Test::Unit.
RULES
ZenTest uses the following rules to figure out what code should be generated:
-
Definition:
-
CUT = Class Under Test
-
TC = Test Class (for CUT)
-
-
TC’s name is the same as CUT w/ “Test” prepended at every scope level.
-
Example: TestA::TestB vs A::B.
-
-
CUT method names are used in CT, with “test_” prependend and optional “_ext” extensions for differentiating test case edge boundaries.
-
Example:
-
A::B#blah
-
TestA::TestB#test_blah_normal
-
TestA::TestB#test_blah_missing_file
-
-
-
All naming conventions are bidirectional with the exception of test extensions.
See ZenTestMapping for documentation on method naming.
Constant Summary collapse
- VERSION =
'4.2'
Instance Attribute Summary collapse
-
#inherited_methods ⇒ Object
Returns the value of attribute inherited_methods.
-
#klasses ⇒ Object
Returns the value of attribute klasses.
-
#missing_methods ⇒ Object
readonly
Returns the value of attribute missing_methods.
-
#test_klasses ⇒ Object
Returns the value of attribute test_klasses.
Class Method Summary collapse
-
.autotest(*klasses) ⇒ Object
Process all the supplied classes for methods etc, and analyse the results.
-
.fix(*files) ⇒ Object
Runs ZenTest over all the supplied files so that they are analysed and the missing methods have skeleton code written.
Instance Method Summary collapse
-
#add_missing_method(klassname, methodname) ⇒ Object
Adds a missing method to the collected results.
-
#analyze ⇒ Object
Walk each known class and test that each method has a test method Then do it in the other direction…
-
#analyze_impl(klassname) ⇒ Object
Checks, for the given class klassname, that each method has a corrsponding test method.
-
#analyze_test(testklassname) ⇒ Object
For the given test class testklassname, ensure that all the test methods have corresponding (normal) methods.
-
#convert_class_name(name) ⇒ Object
Generate the name of a testclass from non-test class so that Foo::Blah => TestFoo::TestBlah, etc.
-
#create_method(indentunit, indent, name) ⇒ Object
create a given method at a given indentation.
-
#generate_code ⇒ Object
Using the results gathered during analysis generate skeletal code with methods raising NotImplementedError, so that they can be filled in later, and so the tests will fail to start with.
-
#get_class(klassname) ⇒ Object
obtain the class klassname, either from Module or using ObjectSpace to search for it.
-
#get_inherited_methods_for(klass, full) ⇒ Object
Return the methods for class klass, as a hash with the method nemas as keys, and true as the value for all keys.
-
#get_methods_for(klass, full = false) ⇒ Object
Get the public instance, class and singleton methods for class klass.
-
#initialize ⇒ ZenTest
constructor
A new instance of ZenTest.
-
#is_test_class(klass) ⇒ Object
Check the class klass is a testing class (by inspecting its name).
-
#load_file(file) ⇒ Object
load_file wraps require, skipping the loading of $0.
-
#methods_and_tests(klassname, testklassname) ⇒ Object
looks up the methods and the corresponding test methods in the collection already built.
-
#process_class(klassname, full = false) ⇒ Object
Does all the work of finding a class by name, obtaining its methods and those of its superclass.
-
#result ⇒ Object
presents results in a readable manner.
-
#scan_files(*files) ⇒ Object
Work through files, collecting class names, method names and assertions.
- #test_to_normal(_name, klassname = nil) ⇒ Object
Methods included from ZenTestMapping
#munge, #normal_to_test, #unmunge
Constructor Details
#initialize ⇒ ZenTest
Returns a new instance of ZenTest.
69 70 71 72 73 74 75 76 77 |
# File 'lib/zentest.rb', line 69 def initialize @result = [] @test_klasses = {} @klasses = {} @error_count = 0 @inherited_methods = Hash.new { |h,k| h[k] = {} } # key = klassname, val = hash of methods => true @missing_methods = Hash.new { |h,k| h[k] = {} } end |
Instance Attribute Details
#inherited_methods ⇒ Object
Returns the value of attribute inherited_methods.
64 65 66 |
# File 'lib/zentest.rb', line 64 def inherited_methods @inherited_methods end |
#klasses ⇒ Object
Returns the value of attribute klasses.
63 64 65 |
# File 'lib/zentest.rb', line 63 def klasses @klasses end |
#missing_methods ⇒ Object (readonly)
Returns the value of attribute missing_methods.
61 62 63 |
# File 'lib/zentest.rb', line 61 def missing_methods @missing_methods end |
#test_klasses ⇒ Object
Returns the value of attribute test_klasses.
62 63 64 |
# File 'lib/zentest.rb', line 62 def test_klasses @test_klasses end |
Class Method Details
.autotest(*klasses) ⇒ Object
Process all the supplied classes for methods etc, and analyse the results. Generate the skeletal code and eval it to put the methods into the runtime environment.
556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 |
# File 'lib/zentest.rb', line 556 def self.autotest(*klasses) zentest = ZenTest.new klasses.each do |klass| zentest.process_class(klass) end zentest.analyze zentest.missing_methods.each do |klass,methods| methods.each do |method,x| warn "autotest generating #{klass}##{method}" end end zentest.generate_code code = zentest.result puts code if $DEBUG Object.class_eval code end |
.fix(*files) ⇒ Object
Runs ZenTest over all the supplied files so that they are analysed and the missing methods have skeleton code written.
544 545 546 547 548 549 550 |
# File 'lib/zentest.rb', line 544 def self.fix(*files) zentest = ZenTest.new zentest.scan_files(*files) zentest.analyze zentest.generate_code return zentest.result end |
Instance Method Details
#add_missing_method(klassname, methodname) ⇒ Object
Adds a missing method to the collected results.
323 324 325 326 327 |
# File 'lib/zentest.rb', line 323 def add_missing_method(klassname, methodname) @result.push "# ERROR method #{klassname}\##{methodname} does not exist (1)" if $DEBUG and not $TESTING @error_count += 1 @missing_methods[klassname][methodname] = true end |
#analyze ⇒ Object
Walk each known class and test that each method has a test method Then do it in the other direction…
453 454 455 456 457 458 459 460 461 462 463 |
# File 'lib/zentest.rb', line 453 def analyze # walk each known class and test that each method has a test method @klasses.each_key do |klassname| self.analyze_impl(klassname) end # now do it in the other direction... @test_klasses.each_key do |testklassname| self.analyze_test(testklassname) end end |
#analyze_impl(klassname) ⇒ Object
Checks, for the given class klassname, that each method has a corrsponding test method. If it doesn’t this is added to the information for that class
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 |
# File 'lib/zentest.rb', line 339 def analyze_impl(klassname) testklassname = self.convert_class_name(klassname) if @test_klasses[testklassname] then methods, testmethods = methods_and_tests(klassname,testklassname) # check that each method has a test method @klasses[klassname].each_key do | methodname | testmethodname = normal_to_test(methodname) unless testmethods[testmethodname] then begin unless testmethods.keys.find { |m| m =~ /#{testmethodname}(_\w+)+$/ } then self.add_missing_method(testklassname, testmethodname) end rescue RegexpError => e puts "# ERROR trying to use '#{testmethodname}' as a regex. Look at #{klassname}.#{methodname}" end end # testmethods[testmethodname] end # @klasses[klassname].each_key else # ! @test_klasses[testklassname] puts "# ERROR test class #{testklassname} does not exist" if $DEBUG @error_count += 1 @klasses[klassname].keys.each do | methodname | self.add_missing_method(testklassname, normal_to_test(methodname)) end end # @test_klasses[testklassname] end |
#analyze_test(testklassname) ⇒ Object
For the given test class testklassname, ensure that all the test methods have corresponding (normal) methods. If not, add them to the information about that class.
370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 |
# File 'lib/zentest.rb', line 370 def analyze_test(testklassname) klassname = self.convert_class_name(testklassname) # CUT might be against a core class, if so, slurp it and analyze it if $stdlib[klassname] then self.process_class(klassname, true) self.analyze_impl(klassname) end if @klasses[klassname] then methods, testmethods = methods_and_tests(klassname,testklassname) # check that each test method has a method testmethods.each_key do | testmethodname | if testmethodname =~ /^test_(?!integration_)/ then # try the current name methodname = test_to_normal(testmethodname, klassname) orig_name = methodname.dup found = false until methodname == "" or methods[methodname] or @inherited_methods[klassname][methodname] do # try the name minus an option (ie mut_opt1 -> mut) if methodname.sub!(/_[^_]+$/, '') then if methods[methodname] or @inherited_methods[klassname][methodname] then found = true end else break # no more substitutions will take place end end # methodname == "" or ... unless found or methods[methodname] or methodname == "initialize" then self.add_missing_method(klassname, orig_name) end else # not a test_.* method unless testmethodname =~ /^util_/ then puts "# WARNING Skipping #{testklassname}\##{testmethodname}" if $DEBUG end end # testmethodname =~ ... end # testmethods.each_key else # ! @klasses[klassname] puts "# ERROR class #{klassname} does not exist" if $DEBUG @error_count += 1 @test_klasses[testklassname].keys.each do |testmethodname| @missing_methods[klassname][test_to_normal(testmethodname)] = true end end # @klasses[klassname] end |
#convert_class_name(name) ⇒ Object
Generate the name of a testclass from non-test class so that Foo::Blah => TestFoo::TestBlah, etc. It the name is already a test class, convert it the other way.
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 |
# File 'lib/zentest.rb', line 187 def convert_class_name(name) name = name.to_s if self.is_test_class(name) then if $r then name = name.gsub(/Test($|::)/, '\1') # FooTest::BlahTest => Foo::Blah else name = name.gsub(/(^|::)Test/, '\1') # TestFoo::TestBlah => Foo::Blah end else if $r then name = name.gsub(/($|::)/, 'Test\1') # Foo::Blah => FooTest::BlahTest else name = name.gsub(/(^|::)/, '\1Test') # Foo::Blah => TestFoo::TestBlah end end return name end |
#create_method(indentunit, indent, name) ⇒ Object
create a given method at a given indentation. Returns an array containing the lines of the method.
439 440 441 442 443 444 445 446 447 448 |
# File 'lib/zentest.rb', line 439 def create_method(indentunit, indent, name) meth = [] meth.push indentunit*indent + "def #{name}" meth.last << "(*args)" unless name =~ /^test/ indent += 1 meth.push indentunit*indent + "raise NotImplementedError, 'Need to write #{name}'" indent -= 1 meth.push indentunit*indent + "end" return meth end |
#generate_code ⇒ Object
Using the results gathered during analysis generate skeletal code with methods raising NotImplementedError, so that they can be filled in later, and so the tests will fail to start with.
469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 |
# File 'lib/zentest.rb', line 469 def generate_code @result.unshift "# Code Generated by ZenTest v. #{VERSION}" if $DEBUG then @result.push "# found classes: #{@klasses.keys.join(', ')}" @result.push "# found test classes: #{@test_klasses.keys.join(', ')}" end if @missing_methods.size > 0 then @result.push "" @result.push "require 'test/unit/testcase'" @result.push "require 'test/unit' if $0 == __FILE__" @result.push "" end indentunit = " " @missing_methods.keys.sort.each do |fullklasspath| methods = @missing_methods[fullklasspath] cls_methods = methods.keys.grep(/^(self\.|test_class_)/) methods.delete_if {|k,v| cls_methods.include? k } next if methods.empty? and cls_methods.empty? indent = 0 is_test_class = self.is_test_class(fullklasspath) klasspath = fullklasspath.split(/::/) klassname = klasspath.pop klasspath.each do | modulename | m = self.get_class(modulename) type = m.nil? ? "module" : m.class.name.downcase @result.push indentunit*indent + "#{type} #{modulename}" indent += 1 end @result.push indentunit*indent + "class #{klassname}" + (is_test_class ? " < Test::Unit::TestCase" : '') indent += 1 meths = [] cls_methods.sort.each do |method| meth = create_method(indentunit, indent, method) meths.push meth.join("\n") end methods.keys.sort.each do |method| next if method =~ /pretty_print/ meth = create_method(indentunit, indent, method) meths.push meth.join("\n") end @result.push meths.join("\n\n") indent -= 1 @result.push indentunit*indent + "end" klasspath.each do | modulename | indent -= 1 @result.push indentunit*indent + "end" end @result.push '' end @result.push "# Number of errors detected: #{@error_count}" @result.push '' end |
#get_class(klassname) ⇒ Object
obtain the class klassname, either from Module or using ObjectSpace to search for it.
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/zentest.rb', line 96 def get_class(klassname) begin klass = Module.const_get(klassname.intern) puts "# found class #{klass.name}" if $DEBUG rescue NameError ObjectSpace.each_object(Class) do |cls| if cls.name =~ /(^|::)#{klassname}$/ then klass = cls klassname = cls.name break end end puts "# searched and found #{klass.name}" if klass and $DEBUG end if klass.nil? and not $TESTING then puts "Could not figure out how to get #{klassname}..." puts "Report to [email protected] w/ relevant source" end return klass end |
#get_inherited_methods_for(klass, full) ⇒ Object
Return the methods for class klass, as a hash with the method nemas as keys, and true as the value for all keys. Unless full is true, leave out the methods for Object which all classes get.
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
# File 'lib/zentest.rb', line 152 def get_inherited_methods_for(klass, full) klass = self.get_class(klass) if klass.kind_of? String klassmethods = {} if (klass.class.method_defined?(:superclass)) then superklass = klass.superclass if superklass then the_methods = superklass.instance_methods(true) # generally we don't test Object's methods... unless full then the_methods -= Object.instance_methods(true) the_methods -= Kernel.methods # FIX (true) - check 1.6 vs 1.8 end the_methods.each do |meth| klassmethods[meth.to_s] = true end end end return klassmethods end |
#get_methods_for(klass, full = false) ⇒ Object
Get the public instance, class and singleton methods for class klass. If full is true, include the methods from Kernel and other modules that get included. The methods suite, new, pretty_print, pretty_print_cycle will not be included in the resuting array.
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'lib/zentest.rb', line 124 def get_methods_for(klass, full=false) klass = self.get_class(klass) if klass.kind_of? String # WTF? public_instance_methods: default vs true vs false = 3 answers # to_s on all results if ruby >= 1.9 public_methods = klass.public_instance_methods(false) public_methods -= Kernel.methods unless full public_methods.map! { |m| m.to_s } public_methods -= %w(pretty_print pretty_print_cycle) klass_methods = klass.singleton_methods(full) klass_methods -= Class.public_methods(true) klass_methods = klass_methods.map { |m| "self.#{m}" } klass_methods -= %w(self.suite new) result = {} (public_methods + klass_methods).each do |meth| puts "# found method #{meth}" if $DEBUG result[meth] = true end return result end |
#is_test_class(klass) ⇒ Object
Check the class klass is a testing class (by inspecting its name).
177 178 179 180 181 182 |
# File 'lib/zentest.rb', line 177 def is_test_class(klass) klass = klass.to_s klasspath = klass.split(/::/) a_bad_classpath = klasspath.find do |s| s !~ ($r ? /Test$/ : /^Test/) end return a_bad_classpath.nil? end |
#load_file(file) ⇒ Object
load_file wraps require, skipping the loading of $0.
80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'lib/zentest.rb', line 80 def load_file(file) puts "# loading #{file} // #{$0}" if $DEBUG unless file == $0 then begin require file rescue LoadError => err puts "Could not load #{file}: #{err}" end else puts "# Skipping loading myself (#{file})" if $DEBUG end end |
#methods_and_tests(klassname, testklassname) ⇒ Object
looks up the methods and the corresponding test methods in the collection already built. To reduce duplication and hide implementation details.
332 333 334 |
# File 'lib/zentest.rb', line 332 def methods_and_tests(klassname, testklassname) return @klasses[klassname], @test_klasses[testklassname] end |
#process_class(klassname, full = false) ⇒ Object
Does all the work of finding a class by name, obtaining its methods and those of its superclass. The full parameter determines if all the methods including those of Object and mixed in modules are obtained (true if they are, false by default).
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 |
# File 'lib/zentest.rb', line 212 def process_class(klassname, full=false) klass = self.get_class(klassname) raise "Couldn't get class for #{klassname}" if klass.nil? klassname = klass.name # refetch to get full name is_test_class = self.is_test_class(klassname) target = is_test_class ? @test_klasses : @klasses # record public instance methods JUST in this class target[klassname] = self.get_methods_for(klass, full) # record ALL instance methods including superclasses (minus Object) # Only minus Object if full is true. @inherited_methods[klassname] = self.get_inherited_methods_for(klass, full) return klassname end |
#result ⇒ Object
presents results in a readable manner.
537 538 539 |
# File 'lib/zentest.rb', line 537 def result return @result.join("\n") end |
#scan_files(*files) ⇒ Object
Work through files, collecting class names, method names and assertions. Detects ZenTest (SKIP|FULL) comments in the bodies of classes. For each class a count of methods and test methods is kept, and the ratio noted.
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 |
# File 'lib/zentest.rb', line 234 def scan_files(*files) assert_count = Hash.new(0) method_count = Hash.new(0) klassname = nil files.each do |path| is_loaded = false # if reading stdin, slurp the whole thing at once file = (path == "-" ? $stdin.read : File.new(path)) file.each_line do |line| if klassname then case line when /^\s*def/ then method_count[klassname] += 1 when /assert|flunk/ then assert_count[klassname] += 1 end end if line =~ /^\s*(?:class|module)\s+([\w:]+)/ then klassname = $1 if line =~ /\#\s*ZenTest SKIP/ then klassname = nil next end full = false if line =~ /\#\s*ZenTest FULL/ then full = true end unless is_loaded then unless path == "-" then self.load_file(path) else eval file, TOPLEVEL_BINDING end is_loaded = true end begin klassname = self.process_class(klassname, full) rescue puts "# Couldn't find class for name #{klassname}" next end # Special Case: ZenTest is already loaded since we are running it if klassname == "TestZenTest" then klassname = "ZenTest" self.process_class(klassname, false) end end # if /class/ end # IO.foreach end # files result = [] method_count.each_key do |classname| entry = {} next if is_test_class(classname) testclassname = convert_class_name(classname) a_count = assert_count[testclassname] m_count = method_count[classname] ratio = a_count.to_f / m_count.to_f * 100.0 entry['n'] = classname entry['r'] = ratio entry['a'] = a_count entry['m'] = m_count result.push entry end sorted_results = result.sort { |a,b| b['r'] <=> a['r'] } @result.push sprintf("# %25s: %4s / %4s = %6s%%", "classname", "asrt", "meth", "ratio") sorted_results.each do |e| @result.push sprintf("# %25s: %4d / %4d = %6.2f%%", e['n'], e['a'], e['m'], e['r']) end end |
#test_to_normal(_name, klassname = nil) ⇒ Object
422 423 424 425 426 427 428 429 430 431 432 433 434 |
# File 'lib/zentest.rb', line 422 def test_to_normal(_name, klassname=nil) super do |name| if defined? @inherited_methods then known_methods = (@inherited_methods[klassname] || {}).keys.sort.reverse known_methods_re = known_methods.map {|s| Regexp.escape(s) }.join("|") name = name.sub(/^(#{known_methods_re})(_.*)?$/) { $1 } unless known_methods_re.empty? name end end end |