欢迎使用 Unittest2doc

English | 中文

Unittest2doc 是一个将 Python 单元测试代码转换为文档的工具。本文档中的单元测试部分就是是使用 Unittest2doc 生成的。

百闻不如一见,我们直接通过本文档的例子来学习 Unittest2doc 的用法。

项目地址为: https://github.com/Fmajor/unittest2doc

项目的文件结构为:

unittest2doc/            # 项目根目录
  src/                   # 源代码目录
    unittest2doc/        # 源代码的包目录
      __init__.py
      unittest2doc.py
      ...
  sphinx-docs/           # 文档目录
    source/              # 文档源文件目录
      conf.py            # 配置文件, 在这里导入要测试的包
      index.rst
      unittests/         # Unittest2doc 会在这里生成rst文件
      src/               # sphinx通过autosummary功能生成的API文档
      ...
    build/               # 文档构建目录, 运行make html 后会在这里生成本文档
  tests/                 # 测试目录, 我们使用Unittest2doc的地方
    test_unittest2doc.py # 测试文件, 其中包括了一个Unittest2doc类, 和其运行示例
    test_pformat_json.py # 我们这里测试了一个结构化输出json的函数
                         #   对于一个结构化输出函数,最好的测试方法是展示出其结果并保存为文档
  pyproject.toml
  README.rst
  • 在项目根目录使用 make unittest

    • 实际执行 python -m unittest discover -s tests -p '*.py' -b

    • 这就是一般的单元测试,我们只关心测试结果,不关心测试过程

  • 在项目根目录使用 make generate-unittest-docs

    • 实际执行 unittest2doc -s tests -p '*.py', 他会直接运行所有满足条件的测试文件

    • 我们每一个测试文件都可以单独执行, 其中的运行参数使得他们可以在运行的时候生成RST格式的sphinx文档, 并且保存到 /sphinx-docs/source/unittests/ 目录下, 最终展现在我们的文档中

这里我们直接展示测试文件

tests/test_unittest2doc.py

其生成的文档为: unittest2doc.unittest2doc.Unittest2Doc

  1import unittest
  2import unittest2doc
  3from unittest2doc import Unittest2Doc, docpprint
  4import time
  5import json
  6import yaml
  7import textwrap
  8from pathlib import Path
  9
 10if 'test Unittest2Doc' and 1:
 11    class Test(unittest.TestCase):
 12        ''' docstring of class, new title
 13            -----------------------------
 14
 15            * Sphinx have already use "=" as title marker
 16            * to form a subtitle, we should use ``-`` as the title marker
 17
 18        '''
 19        def test(s):
 20            a = 1
 21            b = 2
 22            print("# this is a normal unittest.TestCase")
 23            print("# we can use all its assertion methods")
 24            s.assertEqual(a, 1)
 25            s.assertNotEqual(a, b)
 26            s.assertIs(a, 1)
 27            s.assertIsNot(a, b)
 28            s.assertIsNone(None)
 29            s.assertIsNotNone(a)
 30            s.assertTrue(True)
 31            s.assertFalse(False)
 32            
 33        def rst_test_doc(s):
 34            ''' group of tests
 35                --------------
 36
 37                function startswith rst will only provide its docstring to generate docs
 38
 39                we set the ``title_marker`` config to '^', and following tests will be grouped under this title
 40
 41                because rst title markers have these priorities:
 42
 43                * ``=`` (already used by upper Sphinx structure)
 44                * ``-``
 45                * ``^``
 46
 47            '''
 48            unittest2doc.update_config(s, title_marker='^')
 49
 50        #@unittest2doc.stop_after
 51        #@unittest2doc.only
 52        #@unittest2doc.stop
 53        def test_show_variable(s):
 54            """ the title marker is `^` (set in previous function rst_test_doc)
 55
 56                here we test the ``Unittest2Doc.v`` method, to display variables
 57            """
 58            a = 1
 59            b = '2'
 60            c = {
 61                'normal': 'some data',
 62                'secret': 'should be masked',
 63                'subsecret': {
 64                    'good': 1,
 65                    'bad': 0,
 66                    'sub': {
 67                        'good': 1,
 68                        'bad': 0,
 69                    }
 70                }
 71            }
 72            d = [1,2,3]
 73            unittest2doc.v(['a', 'b', 'c', 'd'], locals(), globals(), mask=[
 74                'c.secret',
 75                'c.subsecret.bad',
 76                'c.subsecret.sub.bad',
 77            ])
 78        def test_add_more_doc_0(s):
 79            """ {"open_input":false}
 80
 81                here we close the input block by json setting at first line of docstring
 82
 83                the title marker is still `^` (set in previous function rst_test_doc)
 84
 85            """
 86            pass
 87        def test_add_more_doc_1(s):
 88            """ {"open_output": false}
 89
 90                here we close the output block by json setting at first line of docstring
 91
 92                the title marker is still `^` (set in previous function rst_test_doc)
 93            """
 94            print('here we close the output ')
 95        def test_add_more_doc_2(s):
 96            """ after this, set title level to '-', and the current group is finished
 97            """
 98            unittest2doc.update_config(s, title_marker='-') # set title level to '-' after this test
 99        def test_add_more_doc_3(s):
100            """ this test back to top level (because title_marker is set to '-' at last function)
101            """
102            pass
103        def test_title_marker_for_single_test(s):
104            """ {"title_marker": "^"}
105
106                title marker set by above json is only effective in this function
107
108            """
109            print("# the title_marker ^ is only used in this function, and will not affect other tests")
110            print("# after this test, the title_marker is back to previous '-'")
111        def test_output_as_json(s):
112            """ {"output_highlight": "json"}
113
114                the output is highlighted as ``json``
115
116                the title marker here and below are all the default ``-``
117            """
118            print(json.dumps({"1":1, "2":"2", "3": 3.0, "4":4, "a":[{"1":1, "2":2}, {"3":3, "4":4}]}, indent=2))
119        def test_output_as_yaml(s):
120            """ {"output_highlight": "yaml"}
121
122                the output is highlighted as ``yaml``
123            """
124            # pprint({1:1, '2':'2', '3': 3.0, '4':4, 'a':[{1:1, 2:2}, {3:3, 4:4}]}, expand_all=True, indent_guides=False)
125            docpprint({1:1, '2':'2', '3': 3.0, '4':4, 'a':[{1:1, 2:2}, {3:3, 4:4}]})
126        def test_output_as_python(s):
127            """ {"output_highlight": "python"}
128            """
129            # print(pformat_json({1:1, '2':'2', '3': 3.0, '4':4, 'a':[{1:1, 2:2}, {3:3, 4:4}]}))
130            docpprint({1:1, '2':'2', '3': 3.0, '4':4, 'a':[{1:1, 2:2}, {3:3, 4:4}]})
131            from datetime import datetime
132            from collections import OrderedDict
133            d = [
134                  {
135                    'system_tags': [
136                      OrderedDict([('a', 1), ('b', 2), ('c', 3)]),
137                    ],
138                    'date': datetime.now(),
139                  }
140                ]
141            docpprint(d)
142        @unittest2doc.skip
143        def test_skipped(s):
144            raise Exception('this function should be skipped and we should not get this Exception')
145
146        @unittest2doc.expected_failure
147        def test_with_exception(s):
148            """ {"output_processors": ["no_home_folder"]}
149
150                test with exception, the output string will be processed by ``no_home_folder`` processor defined below
151            """
152            raise Exception('expected exception')
153          
154        def test_add_foldable_output(s):
155          """ add extra foldable text at end of the doc page
156          """
157          print("# add some output")
158          unittest2doc.add_foldable_output(
159            s, # must pass self into add_foldable_output
160            name='some python code',
161            highlight='python',
162            output=textwrap.dedent('''
163                  # some code ...
164                  def func(*args, **kwargs):
165                    pass
166                '''
167            )
168          )
169
170          # some nested data
171          data = {
172            'a': 1,
173            'b': 2,
174            'c': 3,
175            'd': {
176              'a': 1,
177              'b': 2,
178              'c': 3,
179            }
180          }
181          print("# add some output")
182
183          unittest2doc.add_foldable_output(
184            s, # must pass self into add_foldable_output
185            name='some yaml data',
186            highlight='yaml',
187            output=yaml.dump(data, indent=2)
188          )
189          print("# add some output")
190        
191        def test_last(s):
192          """ we use decorator above, make sure that this test is the last one """
193          pass
194
195    class Test2(unittest.TestCase):
196        ''' docstring of class
197            ------------------
198
199            in this class, we test the decorator ``@Unittest2Doc.only``
200
201        '''
202        def setUp(s):
203          print("# this setup function is always called at beginning")
204        def tearDown(s):
205          print("# this tearDown function is always called at end")
206
207        @unittest2doc.only
208        def test_only_1(s):
209          """ when you use ``Unittest2Doc.generate_docs()``, this test will be executed
210
211              Note that if you use ``python -m unittest ...`` framework, all tests will be executed
212              
213              Thus the `only` decorator should only be used during your development and testing,
214              e.g., you just want to test one function and want to skip others for speed
215
216          """
217          pass
218
219        def test_other(s):
220          """ when you use ``Unittest2Doc.generate_docs()``, this test will be skipped because of not @unittest2doc.only decorator
221
222              it will be executed anyway if you use ``python -m unittest ...`` framework
223
224          """
225          pass
226
227        @unittest2doc.only
228        def test_only_2(s):
229          """ when you use ``Unittest2Doc.generate_docs()``, this test will be executed
230          """
231          pass
232    
233    class Test3(unittest.TestCase):
234        """ docstring of class
235            ------------------
236
237            in this class, we test the decorator ``@Unittest2Doc.stop``
238
239        """
240        def setUp(s):
241          print("# this setup function is always called at beginning")
242        def tearDown(s):
243          print("# this tearDown function is always called at end")
244
245        def test_3(s):
246          """ this should be the only test when you use ``Unittest2Doc.generate_docs()``
247
248              we have a @unittest2doc.stop decorator at next test
249
250          """
251          pass
252
253        @unittest2doc.stop
254        def test_2(s):
255          """ stop before this test when you use ``Unittest2Doc.generate_docs()``
256
257              Note that if you use ``python -m unittest ...`` framework, all tests will be executed
258
259              Thus the `stop` decorator should only be used during your development and testing,
260              e.g., you just want to test above function and want to skip others for speed
261          
262          """
263          pass
264        
265        def test_1(s):
266          pass
267
268    class Test4(unittest.TestCase):
269        """ docstring of class
270            ------------------
271
272            in this class, we test the decorator ``@Unittest2Doc.stop_after``
273
274        """
275        def setUp(s):
276          print("# this setup function is always called at beginning")
277        def tearDown(s):
278          print("# this tearDown function is always called at end")
279
280        def test_3(s):
281          """ this should be the executed when you use ``Unittest2Doc.generate_docs()`` """
282          pass
283
284        @unittest2doc.stop_after
285        def test_2(s):
286          """ stop after this test when you use ``Unittest2Doc.generate_docs()``
287
288          """
289          pass
290
291        def test_1(s):
292          """ this should be skipped when you use ``Unittest2Doc.generate_docs()``
293
294              Note that if you use ``python -m unittest ...`` framework, all tests will be executed
295
296              Thus the `stop_after` decorator should only be used during your development and testing,
297              e.g., you just want to test above function and want to skip others for speed
298
299          """
300          pass
301    
302    class Test5(unittest.TestCase):
303        """ docstring of class
304            ------------------
305
306            in this class, we test the unittest decorator (not unittest2doc decorator)
307
308        """
309        def setUp(s):
310          print("# this setup function is always called at beginning")
311        def tearDown(s):
312          print("# this tearDown function is always called at end")
313        
314        @unittest.skip
315        def test_skipped(s):
316          raise Exception('this function should be skipped and we should not get this Exception') 
317
318        @unittest.expectedFailure
319        def test_with_exception(s):
320          raise Exception('expected exception')
321
322
323if __name__ == "__main__":
324    def no_home_folder(output):
325        # filter out `${HOME}/*/unittest2doc` to `${PROJECT_ROOT}/unittest2doc`
326        import os
327        home = os.environ.get('HOME')
328        import re
329        pattern = r"{home}/(?:[^/]+/)*?unittest2doc/".format(home=home)
330        replacement = r"${PROJECT_ROOT}/unittest2doc/"
331        output = re.sub(pattern, replacement, output)
332        return output
333    t = Unittest2Doc(
334        testcase=Test(),
335        name='unittest2doc.unittest2doc.Unittest2Doc.basic',
336        ref=':class:`unittest2doc.unittest2doc.Unittest2Doc`',
337        doc_root=Path(__file__).absolute().parent.parent / 'sphinx-docs/source/unittests',
338        output_processors=dict(
339          no_home_folder=no_home_folder,
340        )
341    )
342    t.generate_docs()
343
344    t2 = Unittest2Doc(
345        testcase=Test2(),
346        name='unittest2doc.unittest2doc.Unittest2Doc.test_decorator_only',
347        ref=':class:`unittest2doc.unittest2doc.Unittest2Doc`',
348        doc_root=Path(__file__).absolute().parent.parent / 'sphinx-docs/source/unittests',
349    )
350    t2.generate_docs()
351
352    t3 = Unittest2Doc(
353        testcase=Test3(),
354        name='unittest2doc.unittest2doc.Unittest2Doc.test_decorator_stop',
355        ref=':class:`unittest2doc.unittest2doc.Unittest2Doc`',
356        doc_root=Path(__file__).absolute().parent.parent / 'sphinx-docs/source/unittests',
357    )
358    t3.generate_docs()
359
360    t4 = Unittest2Doc(
361        testcase=Test4(),
362        name='unittest2doc.unittest2doc.Unittest2Doc.test_decorator_stop_after',
363        ref=':class:`unittest2doc.unittest2doc.Unittest2Doc`',
364        doc_root=Path(__file__).absolute().parent.parent / 'sphinx-docs/source/unittests',
365    )
366    t4.generate_docs()
367
368    t5 = Unittest2Doc(
369        testcase=Test5(),
370        name='unittest2doc.unittest2doc.Unittest2Doc.test_unittest_decorator',
371        ref=':class:`unittest2doc.unittest2doc.Unittest2Doc`',
372        doc_root=Path(__file__).absolute().parent.parent / 'sphinx-docs/source/unittests',
373    )
374    t5.generate_docs()

tests/test_pformat_json.py

其生成的文档为: unittest2doc.formatter

  1import unittest
  2import sys
  3import os
  4import json
  5from pathlib import Path
  6import unittest2doc
  7from unittest2doc import Unittest2Doc, FLog
  8from unittest2doc.formatter.json_formatter import pformat_json
  9
 10class TestJsonFormatter(unittest.TestCase):
 11    """ Test cases for json_formatter module's pformat_json function
 12    """
 13
 14    def setUp(self):
 15        """ here we also test the FLog class, it is a helper class to print function inputs and outputs """
 16        self.flog = FLog(do_print=True, output_suffix='\n', input_suffix=' # ===>')
 17        self.frun = self.flog.frun
 18    
 19    def test_basic_format(self):
 20        """ {"output_highlight": "python"}
 21        """
 22        # Test basic dictionary formatting
 23        data = {"name": "John", "age": 30, "city": "New York"}
 24        # call pformat_json(data) and print the result
 25        self.frun(pformat_json, data)
 26        
 27        # Test basic list formatting
 28        data = [1, 2, 3, "four", 5.0]
 29        # call pformat_json(data) and print the result
 30        self.frun(pformat_json, data)
 31        
 32        # Test simple value
 33        data = "simple string"
 34        # call pformat_json(data) and print the result
 35        self.frun(pformat_json, data)
 36    
 37    def test_nested_structures(self):
 38        """ {"output_highlight": "python"}
 39        """
 40        # Test nested dictionary and list
 41        data = {
 42            "person": {
 43                "name": "Alice",
 44                "details": {
 45                    "age": 28,
 46                    "occupation": "Engineer"
 47                }
 48            },
 49            "hobbies": ["reading", "hiking", {"sport": "tennis"}]
 50        }
 51        self.frun(pformat_json, data)
 52    
 53    def test_dict_title_comment(self):
 54        """ {"output_highlight": "python"}
 55        """
 56        # Test using string comment for dict title
 57        data = {"key1": "value1", "key2": "value2"}
 58        self.frun(pformat_json, data, comments="Dictionary Title")
 59        
 60        # Test using __dtitle__ special key
 61        data = {"key1": "value1", "key2": "value2"}
 62        comments = {"__dtitle__": "Dictionary Title With Special Key"}
 63        self.frun(pformat_json, data, comments=comments)
 64        
 65        # Test multi-line dict title
 66        comments = {"__dtitle__": "First Line\nSecond Line\nThird Line"}
 67        self.frun(pformat_json, data, comments=comments)
 68    
 69    def test_list_title_comment(self):
 70        """ {"output_highlight": "python"}
 71        """
 72        # Test using string comment for list title
 73        data = ["item1", "item2", "item3"]
 74        self.frun(pformat_json, data, comments="List Title")
 75        
 76        # Test using __ltitle__ special key
 77        comments = {"__ltitle__": "List Title With Special Key"}
 78        self.frun(pformat_json, data, comments=comments)
 79        
 80        # Test list prefix and suffix comments
 81        comments = {
 82            "__ltitle__": "List With Prefix and Suffix",
 83            "__lprefix__": ["Prefix Line 1", "Prefix Line 2"],
 84            "__lsuffix__": ["Suffix Line 1", "Suffix Line 2"]
 85        }
 86        self.frun(pformat_json, data, comments=comments)
 87    
 88    def test_specific_element_comments(self):
 89        """ {"output_highlight": "python"}
 90        """
 91        # Test comments for specific dict keys
 92        data = {"name": "Bob", "age": 45, "city": "Boston"}
 93        comments = {
 94            "name": "Person's name",
 95            "age": "Person's age in years",
 96            "city": "City of residence"
 97        }
 98        self.frun(pformat_json, data, comments=comments)
 99        
100        # Test comments for specific list indices
101        data = ["Python", "Java", "JavaScript", "C++"]
102        comments = {
103            0: "My favorite language",
104            2: "Web development language"
105        }
106        self.frun(pformat_json, data, comments=comments)
107    
108    def test_callable_comments(self):
109        """ {"output_highlight": "python"}
110        """
111        # Test callable comments for dict
112        data = {"price": 129.99, "quantity": 5, "discount": 0.15}
113        
114        def calc_total(key, value):
115            if key == "price":
116                return f"Base price: ${value}"
117            elif key == "quantity":
118                return f"Order quantity of {value} units"
119            elif key == "discount":
120                return f"Discount rate of {int(value*100)}%"
121            return ""
122        
123        comments = {
124            "price": calc_total,
125            "quantity": calc_total,
126            "discount": calc_total
127        }
128        self.frun(pformat_json, data, comments=comments)
129    
130    def test_compact_mode(self):
131        """ {"output_highlight": "python"}
132        """
133        # Test compact mode for dict
134        data = {"a": 1, "b": 2, "c": 3, "d": 4, "e": 5, "f": 6}
135        comments = {
136            "__lcompact__": 30,
137            "a": "First item",
138            "c": "Third item",
139            "e": "Fifth item"
140        }
141        self.frun(pformat_json, data, comments=comments)
142        
143        comments['__lcompact__'] = 25
144        self.frun(pformat_json, data, comments=comments)
145
146        comments['__lcompact__'] = 20
147        self.frun(pformat_json, data, comments=comments)
148
149        comments['__lcompact__'] = 15
150        self.frun(pformat_json, data, comments=comments)
151
152        comments.pop('__lcompact__')
153        self.frun(pformat_json, data, comments=comments)
154
155        self.frun(pformat_json, data, comments=comments, compact=15)
156
157        self.frun(pformat_json, data, comments=comments, compact=20)
158
159        self.frun(pformat_json, data, comments=comments, compact=30)
160
161    
162    def test_recursive_comments(self):
163        """ {"output_highlight": "python"}
164        """
165        # Test __lsub__ for all dict elements
166        data = {
167            "user": {
168                "id": 12345,
169                "name": "Alice Smith",
170                "email": "alice@example.com"
171            },
172            "settings": {
173                "theme": "dark",
174                "notifications": True
175            }
176        }
177        comments = {
178            "__dtitle__": "User Profile",
179            "__lsub__": {
180                "id": "Unique identifier",
181                "name": "Full name",
182                "theme": "UI theme preference"
183            }
184        }
185        self.frun(pformat_json, data, comments=comments)
186        
187        # Test __llist__ and __ldict__ for typed elements
188        data = [
189            {"type": "book", "title": "Python Programming"},
190            [1, 2, 3],
191            {"type": "video", "title": "Advanced Python"}
192        ]
193        comments = {
194            "__llist__": {
195                "__lcompact__": 40
196            },
197            "__ldict__": {
198                "type": "Content type",
199                "title": "Content title"
200            }
201        }
202        self.frun(pformat_json, data, comments=comments)
203    
204    def test_custom_indentation(self):
205        """ {"output_highlight": "python"}
206        """
207        # Test custom indentation
208        data = {
209            "outer": {
210                "middle": {
211                    "inner": "value"
212                }
213            }
214        }
215        # Test with different indent values
216        self.frun(pformat_json, data, indent=2)
217        self.frun(pformat_json, data, indent=4)
218    
219    
220    def test_custom_comment_prefix(self):
221        """ {"output_highlight": "python"}
222        """
223        # Test custom comment prefix
224        data = {"name": "John", "age": 30}
225        comments = {
226            "__dtitle__": "Person Info",
227            "name": "Person's name",
228            "age": "Age in years"
229        }
230        self.frun(pformat_json, data, comments=comments, comment_prefix="// ")
231        
232    def test_different_compact_values(self):
233        """ {"output_highlight": "python"}
234        """
235        # Same data with different __lcompact__ values
236        data = {"item1": 100, "item2": 200, "item3": 300, "item4": 400, "item5": 500}
237        comments = {
238            "item1": "First item comment",
239            "item3": "Third item comment",
240            "item5": "Fifth item comment"
241        }
242        
243        # No compact mode
244        print("\nNo compact mode:")
245        self.frun(pformat_json, data, comments=comments)
246        
247        # Very wide compact mode (essentially same as no compact)
248        print("\nCompact mode (width=100):")
249        comments_wide = comments.copy()
250        comments_wide["__lcompact__"] = 100
251        self.frun(pformat_json, data, comments=comments_wide)
252        
253        # Medium compact mode
254        print("\nCompact mode (width=50):")
255        comments_medium = comments.copy()
256        comments_medium["__lcompact__"] = 50
257        self.frun(pformat_json, data, comments=comments_medium)
258        
259        # Narrow compact mode
260        print("\nCompact mode (width=25):")
261        comments_narrow = comments.copy()
262        comments_narrow["__lcompact__"] = 25
263        self.frun(pformat_json, data, comments=comments_narrow)
264    
265    def test_deeply_nested_structure(self):
266        """ {"output_highlight": "python"}
267        """
268        # Create a deeply nested structure to test indentation handling
269        data = {
270            "level1": {
271                "level2": {
272                    "level3": {
273                        "level4": {
274                            "level5": {
275                                "value": "deeply nested value"
276                            },
277                            "array": [1, 2, [3, 4, [5, 6]]]
278                        }
279                    }
280                }
281            }
282        }
283        
284        comments = {
285            "__dtitle__": "Deep Nesting Test",
286            "__lsub__": {
287                "level1": "First level",
288                "level2": "Second level",
289                "level3": "Third level",
290                "level4": "Fourth level",
291                "level5": "Fifth level",
292                "value": "The final value",
293                "array": "Array of values"
294            }
295        }
296        
297        self.frun(pformat_json, data, comments=comments, debug=True)
298    
299    def test_comprehensive_example(self):
300        """ {"output_highlight": "python"}
301        """
302        # Test case combining multiple features
303        data = {
304            "metadata": {
305                "title": "Comprehensive Example",
306                "version": 1.5,
307                "tags": ["test", "example", "comprehensive"]
308            },
309            "configuration": {
310                "enabled": True,
311                "options": {
312                    "debug": False,
313                    "verbose": True,
314                    "timeout": 30
315                }
316            },
317            "data_points": [
318                {"id": 1, "value": 10.5, "label": "Point A"},
319                {"id": 2, "value": 20.7, "label": "Point B"},
320                {"id": 3, "value": 15.3, "label": "Point C"}
321            ],
322            "statistics": {
323                "count": 3,
324                "average": 15.5,
325                "max": 20.7,
326                "min": 10.5
327            }
328        }
329        
330        # Define comprehensive comments
331        def format_stat(key, value):
332            if key == "average":
333                return f"Average value: {value:.1f}"
334            elif key == "max":
335                return f"Maximum value: {value:.1f}"
336            elif key == "min":
337                return f"Minimum value: {value:.1f}"
338            return str(value)
339        
340        comments = {
341            "__dtitle__": "Complete Feature Demonstration",
342            "__lcompact__": 60,
343            "metadata": {
344                "__dtitle__": "Document Metadata",
345                "title": "The title of this example",
346                "tags": "Keywords for categorization"
347            },
348            "configuration": {
349                "__dtitle__": "System Configuration",
350                "__lcompact__": 40,
351                "options": {
352                    "__dtitle__": "Available Options",
353                    "debug": "Enable debug mode",
354                    "verbose": "Show detailed output",
355                    "timeout": "Operation timeout in seconds"
356                }
357            },
358            "data_points": {
359                "__ltitle__": "Measurement Data",
360                "__lprefix__": ["Array of data point objects", "Each with id, value and label"],
361                "__ldict__": {
362                    "id": "Unique identifier",
363                    "value": "Measurement value",
364                    "label": "Display name"
365                }
366            },
367            "statistics": {
368                "__dtitle__": "Statistical Analysis",
369                "count": "Number of data points",
370                "average": format_stat,
371                "max": format_stat,
372                "min": format_stat
373            }
374        }
375        
376        result = pformat_json(data, comments=comments)
377        print(result)
378
379if __name__ == "__main__":
380    t = unittest2doc.Unittest2Doc(
381        testcase=TestJsonFormatter(),
382        name='unittest2doc.formatter.pformat_json',
383        ref=':func:`unittest2doc.formatter.pformat_json`',
384        doc_root=Path(__file__).absolute().parent.parent / 'sphinx-docs/source/unittests',
385    )
386    t.generate_docs()

tests/test_exec_tool.py

其生成的文档为: unittest2doc.utils.exec_tool

 1from pathlib import Path
 2import unittest2doc
 3from unittest2doc import Unittest2Doc
 4from unittest2doc.utils.exec_tool import filter_after_comment_by, load_module, collect_module_attr, load_main
 5
 6""" Note
 7This is a special way to run test cases in the ``if __name__ == "__main__"`` block of a test file (src/unittest2doc/utils/exec_tool.py),
 8Using the helper functions from that file.
 9"""
10
11if __name__ == "__main__":
12    test_module = "unittest2doc.utils.exec_tool"
13    module = load_module(test_module)
14    module_globals = collect_module_attr(module, all=True)
15
16    main = load_main(
17             module,
18             code_filter=filter_after_comment_by("Run unittests"),
19             globals=module_globals,
20             add_code_object=True,
21           )
22
23    t = unittest2doc.Unittest2Doc(
24        testcase=main['TestExecTool'](),
25        name='unittest2doc.utils.exec_tool',
26        ref=':mod:`unittest2doc.utils.exec_tool`',
27        doc_root=Path(__file__).absolute().parent.parent / 'sphinx-docs/source/unittests',
28        open_input=False,
29    )
30    t.generate_docs()

最后,我们来看一下生成的文档

  • API 文档来源于我们的项目源代码, 是sphinx中的autosummary功能生成的, 不是本文的重点

  • 而单元测试文档来源于我们的测试代码, 请自行探索其结果并与测试代码对比

API Documentation and Unittests