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 Minitest or 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.11.1"
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.
-
.usage ⇒ Object
Provide a certain amount of help.
-
.usage_with_exit ⇒ Object
Give help, then quit.
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.
-
#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.
Methods included from ZenTestMapping
#munge, #normal_to_test, #test_to_normal, #unmunge
Constructor Details
#initialize ⇒ ZenTest
Returns a new instance of ZenTest.
80 81 82 83 84 85 86 87 88 |
# File 'lib/zentest.rb', line 80 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.
75 76 77 |
# File 'lib/zentest.rb', line 75 def inherited_methods @inherited_methods end |
#klasses ⇒ Object
Returns the value of attribute klasses.
74 75 76 |
# File 'lib/zentest.rb', line 74 def klasses @klasses end |
#missing_methods ⇒ Object (readonly)
Returns the value of attribute missing_methods.
72 73 74 |
# File 'lib/zentest.rb', line 72 def missing_methods @missing_methods end |
#test_klasses ⇒ Object
Returns the value of attribute test_klasses.
73 74 75 |
# File 'lib/zentest.rb', line 73 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.
581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 |
# File 'lib/zentest.rb', line 581 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. If no files are supplied, splutter out some help.
568 569 570 571 572 573 574 575 |
# File 'lib/zentest.rb', line 568 def self.fix(*files) ZenTest.usage_with_exit if files.empty? zentest = ZenTest.new zentest.scan_files(*files) zentest.analyze zentest.generate_code return zentest.result end |
.usage ⇒ Object
Provide a certain amount of help.
525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 |
# File 'lib/zentest.rb', line 525 def self.usage puts <<-EO_USAGE usage: #{File.basename $0} [options] test-and-implementation-files... 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 Minitest or Test::Unit. 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. options: -h display this information -v display version information -r Reverse mapping (ClassTest instead of TestClass) -e (Rapid XP) eval the code generated instead of printing it -t test/unit generation (default is minitest). EO_USAGE end |
.usage_with_exit ⇒ Object
Give help, then quit.
559 560 561 562 |
# File 'lib/zentest.rb', line 559 def self.usage_with_exit self.usage exit 0 end |
Instance Method Details
#add_missing_method(klassname, methodname) ⇒ Object
Adds a missing method to the collected results.
326 327 328 329 330 |
# File 'lib/zentest.rb', line 326 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…
442 443 444 445 446 447 448 449 450 451 452 |
# File 'lib/zentest.rb', line 442 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
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 |
# File 'lib/zentest.rb', line 342 def analyze_impl(klassname) testklassname = self.convert_class_name(klassname) if @test_klasses[testklassname] then _, 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 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.
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 421 422 423 |
# File 'lib/zentest.rb', line 373 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.
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 |
# File 'lib/zentest.rb', line 190 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.
428 429 430 431 432 433 434 435 436 437 |
# File 'lib/zentest.rb', line 428 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.
458 459 460 461 462 463 464 465 466 467 468 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 |
# File 'lib/zentest.rb', line 458 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 "" if $t then @result.push "require 'test/unit/testcase'" @result.push "require 'test/unit' if $0 == __FILE__" else @result.push "require 'minitest/autorun'" end @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) clsname = $t ? "Test::Unit::TestCase" : "Minitest::Test" superclass = is_test_class ? " < #{clsname}" : '' @result.push indentunit*indent + "class #{fullklasspath}#{superclass}" 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" @result.push '' end @result.push "# Number of errors detected: #{@error_count}" @result.push '' end |
#get_class(klassname) ⇒ Object
obtain the class klassname
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/zentest.rb', line 106 def get_class(klassname) klass = nil begin klass = klassname.split(/::/).inject(Object) { |k,n| k.const_get n } puts "# found class #{klass.name}" if $DEBUG rescue NameError 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.
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
# File 'lib/zentest.rb', line 155 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.
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/zentest.rb', line 127 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).
180 181 182 183 184 185 |
# File 'lib/zentest.rb', line 180 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.
91 92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/zentest.rb', line 91 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.
335 336 337 |
# File 'lib/zentest.rb', line 335 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).
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 |
# File 'lib/zentest.rb', line 215 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.
520 521 522 |
# File 'lib/zentest.rb', line 520 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.
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 321 322 323 |
# File 'lib/zentest.rb', line 237 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 |