5151 LogEvent ,
5252 TestResult ,
5353)
54+ from testtools .testresult .real import (
55+ ExtendedToStreamDecorator ,
56+ StreamSummary ,
57+ )
5458
5559from .helpers import (
5660 AsText ,
@@ -2353,16 +2357,36 @@ def _run_case(self, case):
23532357 case .run (result )
23542358 return result
23552359
2360+ def _outcomes (self , result , case ):
2361+ """The (event name, subtest label) pairs from a run.
2362+
2363+ Labels are relative to ``case``'s own id, so that tests can assert on
2364+ the subtest description without depending on the enclosing qualname.
2365+ """
2366+ prefix = case .id ()
2367+ outcomes = []
2368+ for event in result ._events :
2369+ if event [0 ] in ("startTest" , "stopTest" ):
2370+ continue
2371+ reported = event [1 ].id ()
2372+ if reported .startswith (prefix ):
2373+ reported = reported [len (prefix ) :].lstrip ()
2374+ outcomes .append ((event [0 ], reported ))
2375+ return outcomes
2376+
23562377 def test_passing_subtests (self ):
23572378 class Case (TestCase ):
23582379 def test_it (self ):
23592380 for i in (0 , 2 , 4 ):
23602381 with self .subTest (i = i ):
23612382 self .assertEqual (i % 2 , 0 )
23622383
2363- result = self ._run_case (Case ("test_it" ))
2364- self .assertIn ("addSuccess" , [e [0 ] for e in result ._events ])
2365- self .assertEqual ([], result .failures )
2384+ case = Case ("test_it" )
2385+ result = self ._run_case (case )
2386+ self .assertEqual (
2387+ [("addSuccess" , "" )],
2388+ self ._outcomes (result , case ),
2389+ )
23662390
23672391 def test_single_failure (self ):
23682392 class Case (TestCase ):
@@ -2371,11 +2395,12 @@ def test_it(self):
23712395 with self .subTest (i = i ):
23722396 self .assertEqual (i % 2 , 0 )
23732397
2374- result = self ._run_case (Case ("test_it" ))
2375- self .assertNotIn ("addSuccess" , [e [0 ] for e in result ._events ])
2376- self .assertEqual (1 , len (result .failures ))
2377- subtest = result .failures [0 ][0 ]
2378- self .assertIn ("(i=1)" , str (subtest ))
2398+ case = Case ("test_it" )
2399+ result = self ._run_case (case )
2400+ self .assertEqual (
2401+ [("addFailure" , "(i=1)" )],
2402+ self ._outcomes (result , case ),
2403+ )
23792404
23802405 def test_multiple_failures (self ):
23812406 class Case (TestCase ):
@@ -2384,16 +2409,14 @@ def test_it(self):
23842409 with self .subTest (i = i ):
23852410 self .assertEqual (i % 2 , 0 )
23862411
2387- result = self ._run_case (Case ("test_it" ))
2388- self .assertEqual (2 , len (result .failures ))
2389- descriptions = [str (f [0 ]) for f in result .failures ]
2390- self .assertTrue (
2391- any ("(i=1)" in d for d in descriptions ),
2392- f"Expected a failure for (i=1), got { descriptions } " ,
2393- )
2394- self .assertTrue (
2395- any ("(i=3)" in d for d in descriptions ),
2396- f"Expected a failure for (i=3), got { descriptions } " ,
2412+ case = Case ("test_it" )
2413+ result = self ._run_case (case )
2414+ self .assertEqual (
2415+ [
2416+ ("addFailure" , "(i=1)" ),
2417+ ("addFailure" , "(i=3)" ),
2418+ ],
2419+ self ._outcomes (result , case ),
23972420 )
23982421
23992422 def test_failure_continues_loop (self ):
@@ -2418,34 +2441,38 @@ def test_it(self):
24182441 with self .subTest (msg = "my label" , x = 42 ):
24192442 self .fail ("boom" )
24202443
2421- result = self ._run_case (Case ("test_it" ))
2422- self .assertEqual (1 , len (result .failures ))
2423- description = str (result .failures [0 ][0 ])
2424- self .assertIn ("[my label]" , description )
2425- self .assertIn ("(x=42)" , description )
2444+ case = Case ("test_it" )
2445+ result = self ._run_case (case )
2446+ self .assertEqual (
2447+ [("addFailure" , "[my label] (x=42)" )],
2448+ self ._outcomes (result , case ),
2449+ )
24262450
24272451 def test_multiple_params (self ):
24282452 class Case (TestCase ):
24292453 def test_it (self ):
24302454 with self .subTest (a = 1 , b = "two" ):
24312455 self .fail ("boom" )
24322456
2433- result = self ._run_case (Case ("test_it" ))
2434- self .assertEqual (1 , len (result .failures ))
2435- description = str (result .failures [0 ][0 ])
2436- self .assertIn ("a=1" , description )
2437- self .assertIn ("b='two'" , description )
2457+ case = Case ("test_it" )
2458+ result = self ._run_case (case )
2459+ self .assertEqual (
2460+ [("addFailure" , "(a=1, b='two')" )],
2461+ self ._outcomes (result , case ),
2462+ )
24382463
24392464 def test_no_params (self ):
24402465 class Case (TestCase ):
24412466 def test_it (self ):
24422467 with self .subTest ():
24432468 self .fail ("boom" )
24442469
2445- result = self ._run_case (Case ("test_it" ))
2446- self .assertEqual (1 , len (result .failures ))
2447- description = str (result .failures [0 ][0 ])
2448- self .assertIn ("(<subtest>)" , description )
2470+ case = Case ("test_it" )
2471+ result = self ._run_case (case )
2472+ self .assertEqual (
2473+ [("addFailure" , "(<subtest>)" )],
2474+ self ._outcomes (result , case ),
2475+ )
24492476
24502477 def test_nested_subtests (self ):
24512478 class Case (TestCase ):
@@ -2456,16 +2483,16 @@ def test_it(self):
24562483 with self .subTest (b = b ):
24572484 self .assertEqual (a , b )
24582485
2459- result = self . _run_case ( Case ("test_it" ) )
2460- self .assertEqual ( 4 , len ( result . failures ) )
2461- descriptions = [ str ( f [ 0 ]) for f in result . failures ]
2462- self . assertTrue (
2463- any ( " a=1" in d and " b=3" in d for d in descriptions ),
2464- f"Expected a failure with both a=1 and b=3, got { descriptions } " ,
2465- )
2466- self . assertTrue (
2467- any ( "a=2" in d and "b=4" in d for d in descriptions ) ,
2468- f"Expected a failure with both a=2 and b=4, got { descriptions } " ,
2486+ case = Case ("test_it" )
2487+ result = self ._run_case ( case )
2488+ self . assertEqual (
2489+ [
2490+ ( "addFailure" , "( a=1, b=3)" ),
2491+ ( "addFailure" , "( a=1, b=4)" ) ,
2492+ ( "addFailure" , "(a=2, b=3)" ),
2493+ ( "addFailure" , "(a=2, b=4)" ),
2494+ ] ,
2495+ self . _outcomes ( result , case ) ,
24692496 )
24702497
24712498 def test_skip_inside_subtest (self ):
@@ -2482,9 +2509,186 @@ def test_it(self):
24822509 self .reached .append (i )
24832510
24842511 case = Case ("test_it" )
2485- self ._run_case (case )
2512+ result = self ._run_case (case )
24862513 self .assertEqual ([0 , 2 ], case .reached )
2487- self .assertEqual (1 , len (case ._subtest_skips ))
2514+ # The skipped subtest is reported on its own; the parent is not
2515+ # additionally reported as a success.
2516+ self .assertEqual (
2517+ [("addSkip" , "(i=1)" )],
2518+ self ._outcomes (result , case ),
2519+ )
2520+
2521+ def test_skip_uses_custom_skip_exception (self ):
2522+ class CustomSkip (Exception ):
2523+ pass
2524+
2525+ class Case (TestCase ):
2526+ skipException = CustomSkip
2527+
2528+ def test_it (self ):
2529+ with self .subTest (i = 1 ):
2530+ self .skipTest ("skip this one" )
2531+
2532+ case = Case ("test_it" )
2533+ result = self ._run_case (case )
2534+ self .assertEqual (
2535+ [("addSkip" , "(i=1)" )],
2536+ self ._outcomes (result , case ),
2537+ )
2538+
2539+ def test_error_is_reported_as_error (self ):
2540+ class Case (TestCase ):
2541+ def test_it (self ):
2542+ with self .subTest (i = 1 ):
2543+ raise ValueError ("boom" )
2544+
2545+ case = Case ("test_it" )
2546+ result = self ._run_case (case )
2547+ self .assertEqual (
2548+ [("addError" , "(i=1)" )],
2549+ self ._outcomes (result , case ),
2550+ )
2551+
2552+ def test_expected_failure_inside_subtest (self ):
2553+ class Case (TestCase ):
2554+ def test_it (self ):
2555+ with self .subTest (i = 1 ):
2556+ self .expectFailure ("known bug" , self .assertEqual , 1 , 2 )
2557+
2558+ case = Case ("test_it" )
2559+ result = self ._run_case (case )
2560+ self .assertEqual (
2561+ [("addExpectedFailure" , "(i=1)" )],
2562+ self ._outcomes (result , case ),
2563+ )
2564+
2565+ def test_details_are_reported_with_the_subtest (self ):
2566+ class Case (TestCase ):
2567+ def test_it (self ):
2568+ with self .subTest (i = 1 ):
2569+ self .addDetail ("clue" , text_content ("important diagnostic" ))
2570+ self .fail ("boom" )
2571+
2572+ case = Case ("test_it" )
2573+ result = self ._run_case (case )
2574+ [(_ , _ , details )] = [e for e in result ._events if e [0 ] == "addFailure" ]
2575+ self .assertEqual ("important diagnostic" , details ["clue" ].as_text ())
2576+
2577+ def test_details_do_not_leak_to_the_parent (self ):
2578+ class Case (TestCase ):
2579+ def test_it (self ):
2580+ with self .subTest (i = 1 ):
2581+ self .addDetail ("clue" , text_content ("subtest only" ))
2582+ self .fail ("boom" )
2583+ self .assertNotIn ("clue" , self .getDetails ())
2584+
2585+ case = Case ("test_it" )
2586+ result = self ._run_case (case )
2587+ self .assertEqual (
2588+ [("addFailure" , "(i=1)" )],
2589+ self ._outcomes (result , case ),
2590+ )
2591+
2592+ def test_multiple_exceptions_are_unpacked (self ):
2593+ class Case (TestCase ):
2594+ def test_it (self ):
2595+ with self .subTest (i = 1 ):
2596+ raise MultipleExceptions (
2597+ (ValueError , ValueError ("one" ), None ),
2598+ (KeyError , KeyError ("two" ), None ),
2599+ )
2600+
2601+ case = Case ("test_it" )
2602+ result = self ._run_case (case )
2603+ self .assertEqual (
2604+ [
2605+ ("addError" , "(i=1)" ),
2606+ ("addError" , "(i=1)" ),
2607+ ],
2608+ self ._outcomes (result , case ),
2609+ )
2610+
2611+ def test_failfast_stops_at_first_failing_subtest (self ):
2612+ class Case (TestCase ):
2613+ def __init__ (self , * args , ** kwargs ):
2614+ super ().__init__ (* args , ** kwargs )
2615+ self .iterations = []
2616+
2617+ def test_it (self ):
2618+ for i in range (4 ):
2619+ with self .subTest (i = i ):
2620+ self .iterations .append (i )
2621+ self .fail ("boom" )
2622+
2623+ case = Case ("test_it" )
2624+ result = ExtendedTestResult ()
2625+ result .failfast = True
2626+ case .run (result )
2627+ self .assertEqual ([0 ], case .iterations )
2628+ self .assertEqual (
2629+ [("addFailure" , "(i=0)" )],
2630+ self ._outcomes (result , case ),
2631+ )
2632+
2633+ def test_keyboard_interrupt_propagates (self ):
2634+ class Case (TestCase ):
2635+ def test_it (self ):
2636+ with self .subTest (i = 1 ):
2637+ raise KeyboardInterrupt
2638+
2639+ self .assertRaises (KeyboardInterrupt , Case ("test_it" ).run , ExtendedTestResult ())
2640+
2641+ def test_subtest_is_not_runnable (self ):
2642+ class Case (TestCase ):
2643+ def test_it (self ):
2644+ with self .subTest (i = 1 ):
2645+ self .fail ("boom" )
2646+
2647+ case = Case ("test_it" )
2648+ result = self ._run_case (case )
2649+ [(_ , subtest , _ )] = [e for e in result ._events if e [0 ] == "addFailure" ]
2650+ self .assertRaises (NotImplementedError , subtest .runTest )
2651+
2652+ def test_stream_result_counts_one_test (self ):
2653+ # A subtest outcome is attached to the parent test rather than
2654+ # opening a stream record of its own, as unittest does.
2655+ class Case (TestCase ):
2656+ def test_it (self ):
2657+ with self .subTest (i = 1 ):
2658+ self .fail ("boom" )
2659+
2660+ summary = StreamSummary ()
2661+ decorator = ExtendedToStreamDecorator (summary )
2662+ decorator .startTestRun ()
2663+ Case ("test_it" ).run (decorator )
2664+ decorator .stopTestRun ()
2665+ self .assertEqual (1 , summary .testsRun )
2666+ self .assertEqual (False , summary .wasSuccessful ())
2667+
2668+ def test_stream_result_counts_one_test_for_skip (self ):
2669+ class Case (TestCase ):
2670+ def test_it (self ):
2671+ with self .subTest (i = 1 ):
2672+ self .skipTest ("skip this one" )
2673+
2674+ summary = StreamSummary ()
2675+ decorator = ExtendedToStreamDecorator (summary )
2676+ decorator .startTestRun ()
2677+ Case ("test_it" ).run (decorator )
2678+ decorator .stopTestRun ()
2679+ self .assertEqual (1 , summary .testsRun )
2680+ self .assertEqual (True , summary .wasSuccessful ())
2681+
2682+ def test_str_matches_unittest_format (self ):
2683+ class Case (TestCase ):
2684+ def test_it (self ):
2685+ with self .subTest (i = 1 ):
2686+ self .fail ("boom" )
2687+
2688+ case = Case ("test_it" )
2689+ result = self ._run_case (case )
2690+ [(_ , subtest , _ )] = [e for e in result ._events if e [0 ] == "addFailure" ]
2691+ self .assertEqual (f"{ case } (i=1)" , str (subtest ))
24882692
24892693
24902694def test_suite ():
0 commit comments