Fork me on GitHub

ABAP单元测试最佳实践

 

  本文包含了我在开发项目中经历过的实用的ABAP单元测试指导方针。我把它们安排成为问答的风格,欢迎任何人添加更多的Q&A's,以完成这个列表。

 

  • 在我的项目中,只使用传统的ABAP report。所以很不幸我不能使用ABAP单元测试了,是吗?
    有个好消息:无论你正在使用哪一种ABAP代码对象进行开发,都可以通过添加单元测试使得它更加稳定和更易于扩展。对于reports,模块池(module pools)和函数组(function groups),可以通过添加手写本地类的方式添加单元测试。假设一个简单的情形,在一个report中你想要测试子程序xyz的最直接调用,下面的代码骨架就可以做到,这段代码可以定义为代码模板,以便于插入到report。
    class lcl_test definition for testing  "#AU Duration Short
      inheriting from cl_aunit_assert. "#AU Risk_Level Harmless
      private section.
        methods test_xyz_simple_call for testing.
    endclass.
     
    class lcl_test implementation.
      method test_xyz_simple_call.
    * Setup parameters for the call...
    * Perform the call
        perform xyz using ...
    * Check returned values
        assert_equals( act = ... exp = ... ).
      endmethod.
    endclass.

    当然,使用ABAP面向对象有很多好处,比如,会有ABAP类的单元测试模板的自动生成功能。同样地,生产代码和测试代码的分界会更清晰。测试类声生成在一个用于单元测试的“包含(incluede)”部分,会同其它内容隔离。如果出于某些原因不需要使用这些类,你依然拥有单元测试支持。

 

  • 不幸的是,我的客户的开发系统中的主数据质量太差了,以至于我用不了单元测试。
    又有好的消息:尽管客户的开发系统有着糟糕的数据质量,你还是可以做单元测试!单元测试的最大优势之一,就是可以独立地测试单一的代码单元——测试不依赖任何数据库条目,不依赖其它中途调用的函数模块。如果测试在500客户端没问题,那它在000客户端同样可以运行得很好。

  

  • 不是真的想让我为所有开发过的代码对象写单元测试吧?

    不,并没有。为某些代码写单元测试会导致时间的浪费。它们是:

    1. 自动生成的代码,比如视图维护函数组,静态系统信息报表,BSP扩展基础类,以及其它相似的东西。
    2. 大多数数据库查询。在多数情况下,数据库查询不应该在单元测试内执行(关于该点请看下文)。有一些例外,比如DAOs(Data Access Object)。这些是单个数据库的专家(?)。在某些特殊情况下,为了测试功能性而创建测试条目(并且在teardown阶段移除)是行得通的。
    3. 连接dynpro和abap代码的代码。有一些需要重定向dynpro的胶水代码,就像PAI(Process After Input)中一个特定的对ABAP代码块的链式请求所起的作用那样。通常是不值得花费努力为这样的胶水代码进行单元测试的。

 

  • 某个类做的事情是不重要的,不值得为它做测试。
    也许你是对的,但通常,你错了。只是你认为你的代码不重要,因为你只是完成了它的编写。经验表明,一年后,先前不重要的代码,看起来再也不会对你不重要了。你的同事也同样不会认为它不重要。如果你只是实现了一个适配器类,将一个数据格式映射为另一个,接着调用一个API,也许你是对的:对这样的类进行单元测试也许是过度工程。但是随着源代码体积的上升,看起来不重要的代码也许包含某些bug,这种bug只有在被调用的时候才会显现。为什么不实现一个可以自动检查期望结果的调用呢?这样做可以保证该类在任何时候都工作正常。

 

  • 单元测试需要同测试驱动开发(TDD)共同进行吗?
    基本上,TDD是一种意为“首先实现测试,之后添加可以使得测试通过的生产代码”的编程实践。这是一个“乒乓球”过程,你将总是在新的测试代码和新的生产代码间转向。你不需要实践TDD,但是如果你习惯了它,会在很大程度上帮助你避免bug,并因此变得更有效率。即使不使用测试驱动,你依然会受益于单元测试:可以向已存在的代码对象添加事后比较检验(post-hoc test)。 

 

  • 单元的外部测试怎么样?使用单独的测试对象。
    可以在一个类的属性标签中指定其作为单元测试类。但是这应当用于通过继承来提取几个相似的本地单元测试类的测试代码的情况,而不应用在单个单元的测试上面。通常,在外部测试一个单元是不建议的做法,因为这使得单元测试在工作台菜单路径中的“模块测试”里成为不可用的状态。如果你的类被某人改动了,他也许没有意识到代码应当通过外部程序的测试。因此更好的做法是把它包含在生产代码所在的同一个对象当中。

 

  • 如果我不测试所有的代码,测试覆盖中会出现断层!
    虽然单元测试是个很有用的工具,但并不能回应所有的需求。我在上面已经提到,这种断层不建议使用单元测试处理,而是应该使用被称为集成测试的其它技术进行覆盖,比如eCATT, QTP或者其它。

 

  • 我该怎样设计单元测试?

    要点在于:应该将它们设计的尽可能简单。单元测试同样起着单元功能文档的作用。同样地,如果执行修改后,单元测试失败,会很容易从代码中看出哪个功能失败了。尝试避免测试方法中的多余代码。将重复代码包装到方法中甚至宏之中,以保证在测试下功能的实质更加可读。直率地命名变量、方法、类和宏,使得代码在测试时尽可能的具有表达力。单元的每个特性,都需要可以按照以下三步测试:

    1. 建立测试数据——填充接口参数的内表或属性,以及/或者
    2. 调用测试方法——通常正好是对公共方法的调用。
    3. 检测方法输出的异常。
      这三步应当被包含在一个测试方法中。在每个测试方法附近,通常是测试对象构建的地方,会有一个建立步骤(对于类的每一个方法都是相同的),如果有需要的话,会提供桩。同样的,每个测试方法的调用后伴随着一个teardown调用。

 

  • 我怎样识别自己的方法其实在被单元测试调用,而不是真的用户?我想在这种情形下做点不同的事情。
    别这样!不要将生产代码和测试代码混在一起,如果想要为了测试而消除生产代码中的一部分,应当使用桩和依赖注入来代替。但是,在生产代码使用一个“测试模式”的标识,会破坏单元测试的概念,并且导致的代码变得更糟糕。

 

  • 我要怎样组织自己的代码?
    没有用于组织单元测试的通行方案。有时让每个方法有一个单元测试类、每个输入数据的等价类有一个测试方法是好的做法。但这不是一般的规则。一般来说,测试方法在正交时会变得有用:理想情况下,每个方法测试测试一个不依赖其它存在的单一功能。不要让测试方法负担过多的断言。

 

  • 如何测试一个将数据库查询和它自己的业务逻辑混合到一起并且调用了其它函数模块的程序(方法/函数模块)?
    The redefined helper classes like lcl_api_test and lcl_db_test is what the test people call stubs.首先让代码成为可测试的,例如使用桩:将数据库查询和函数模块调用包装到本地帮助类中(数据库方面我使用LCL_DB,调用其它代码单元方面我使用LCL_API ),提取这些代码到自己的方法里。为这些方法使用具有表达性的名字,使用适配器模式为它们设计一个良好的接口。之后你的LCL_API和LCL_DB将只包含外部函数模块调用和数据库操作(select, insert, update, enqueue, ...),也许会有几行映射代码,用于将你设计的好的接口映射到你调用的模块的传统接口。
    在你的对象中应有像go_api和godb这样的全局的帮助类实例可用。在测试方法中重定义其方法,控制他们的行为。像lcl_api_test和lcl_db_test这样的重定义过的帮助类就是测试人员所说的的“桩”。

 

  • 听起来是复杂的。
    你说得对,它不是直接的。为了保持测试代码简单可理解,你应当尝试在任何时候尽可能避免桩的使用。可以通过在业务逻辑、API调用和数据库操作之间提供更好的分隔,来避免桩。例如,不在相同的方法里面查询数据、对数据执行检查。可以首先查询数据,接着将数据条目作为导入参数在自己的方法里进行检查。通过这种方法让代码变得可测试,通常——作为副作用——会提高其可读性。

 

  • 我应该测试受保护方法或者私有方法么?
    通常不用。通常,你会关注一个类的公共界面。私有属性或方法也许会在重构期间消失,或者被其它组件代替。如果它对公共方法调用没有任何影响,就算删除它,也许都是安全的;如果它对公共方法调用有影响,那就测试公共方法——保持未来重构的自由。如果测试私有方法,接着,想要改变这些组件的时候,就不得不改变它们的单元测试,这导致代码的可变性很差。

 

  • 好的——但是,我在某种特别的状况下(blabla...)真的很需要测试私有方法和受保护的方法。我要怎样提供这个?
    因为,像任何其它类一样,本地类是独立于它们的包含工作台类的,你需要声明本地测试类为包含类的友元。如果zcl_testee是包含类,lcl_test是单元测试类,需要在本地测试类中添加如下代码:
    class lcl_test definition deferred.
    class zcl_testee definition local friends lcl_test.
    ...
    class lcl_test definition for testing ...
    ...

 

  • 我的单元测试包含语法错误,但是它对生产类没影响,因为单元测试只在开发系统中进行。对吗?
    (warning) 不是的。单元测试不可以在生产系统中执行,但是类中的单元部分里面的语法错误会破坏完整的类,导致访问类的任何属性或方法时会出现SYNTAX_ERROR的short dump。

 

  • 我的测试对象是一个单例。为了避免副作用,我想至少对每个方法的测试得创建一个新的实例。
    如果你的单例包含全局数据,它们也许会被测试改变,在测试调用之间生成丑陋的依赖。你可以在测试期间通过属性“create public”创建一个对象的子类,按照如下方法进行。
    如果你只是需要类行为的这种改变,你甚至不需要子类的“class...implementation”部分。
    class lcl_testee definition inheriting from zcl_someclass create public.
    endclass.
    ...
    class lcl_test implementation.
      method setup.
        create object go_testee type lcl_testee.
      endmethod.
    endclass.

    记着,无论如何,问题不会由单元测试而是全局数据引起。单元测试只是发现问题,而不是导致问题。因此最佳的解决方式是排除类中的全局数据。

 

  • 我要怎样做能让我的测试代码变得更加可读?
    • 无论在任何时候,尽可能地使用隐式的“函数”表示法进行方法调用,特别是像assert( ), assert_initial(), assert_subrc()等等这种调用。
    • 如果你不需要测试类的继承层次(为什么需要?),你也许会让测试类继承自cl_aunit_assert。可以像这样写:

      assert_subrc( sy-subrc ).
      而不是

      call method cl_aunit_assert=>assert_subrc
        exporting
          act = sy-subrc.
      • 如果调用是复杂的(比如含有很多参数),使用宏填充内表和调用测试方法。省去调用自身的重复代码,也省去了用于填充内表的本地变量比如工作区。我们使用一种宏包含子程序池的结合来填充内表,减少了用于工作区的辅助本地变量的需要。
        如果你需要一个例子:这里是一个用于解析器的测试方法,可以将指定的包装规则转换为自由文本并将其放入内表中,内表中包含以预定义格式存在的相关信息。建立自由文本、调用解析器方法、检查结果内表的特定组件,这三种行为,在约20个不同方法中是重复的,只有自由文本的内容和修改的内表中的预期结果会改变。
        宏_assert_n_fields_in_row检查指定内表的指定行的指定的组件含有指定的值!
    • method test_2_lief_2_pal.
       
      * Test assignment of deliveries to handling units
       
          _set_code:
            `1. Palette  (                 `,
            `  1. Lieferung, 1. Pos, 50%   `,
            `  )                           `,
            `2. Palette  (                 `,
            `  2. Lieferung, 2. Pos, Rest  `,
            ` )                            `.
       
          _call_parser.
       
          _assert_rows 'Pack data' gt_packdata_template 2.
       
          _assert_n_fields_in_row 'Pack data' gt_packdata_template 'exidv;vepos;vbeln;posnr;vemng;vemeh;unvel' :
             1 'E1;1;1;1;50;!%;',
             2 'E2;1;2;2;REST;;'.
       
        endmethod.
      用这种方式提取重复的代码,减少上面提到过的用于“建立——测试调用——校验”三个步骤的方法,以明确受测试的功能。
    • 读一些好书,比如Martin Fowler的《重构》,或者Robert C. Martin的《代码整洁之道》以获取更多关于代码如何变得更加可读的思想。

 

  • 使用宏对调试不利吗?
     视情况而定,如果只是使用宏来“去掉噪音”,比如,用来提取总是一样并且频繁使用到的代码序列,那么在调试器里面使用F6跳过它的执行就不是问题。如果你有一个隐藏了像上面例子中的_call_parser一样的方法调用的宏,你可以使用F5进入该方法,即使调用隐藏在宏里面。此外,在这种情形下,你只是失去了代码中无趣的部分。

 

  • 在一个作业中周期性地运行单元测试是有意义的吗?
    通常,单元测试和新代码的开发相关联。与集成测试相反,在夜间作业运行它们并不让人意外,因为结果只在代码改变的时候改变,因此代码的最后修改者应该知道结果——如果他测试了他的单元!如果你的团队中有不使用单元测试的开发者,或者代码的最后一个修改者仅仅是忘记了运行单元测试,有个作业来通知失败,会很不错(比如通过发送邮件给TADIR的拥有者)。你可以使用代码检查器(code inspector)运行单元测试。别忘记在单元测试类定义中的有关风险等级的伪代码注释和期间,因为,否则的话,代码检查其也许会不执行测试:
    class lcl_test definition for testing  "#AU Duration Short
      inheriting from cl_aunit_assert. "#AU Risk_Level Harmless
     ...



  • 在传输请求将要发布的时候检查单元测试是可行的吗?
    可以,而且我认为它很有用。最简单的达成方式是打开传输发布的代码检查器检查,并在检查变量中选择“单元测试”。
    在我们的实践中,我选择了一个更 复杂的方式,使用传输组织器的BAdI和一个函数模块调用单元测试。虽然这个功能没有得到SAP的保障(短文本中包含危险修订“for SAP only”),它还是看起来工作的相当好。我们从两年前开始使用它,到现在也没出问题。方法cl_aunit_prog_info=> contain_programs_testcode( )也许可以用于找出特定的程序(根据指定主程序的报表源的名字)是否包含单元测试。如果仅仅是程序、类或者函数模块的一部分改变了,你也许不得不找出LIMU的父对象。为实现这点,可以使用函数模块TR_CHECK_TYPE。

 

本文链接:http://www.cnblogs.com/hhelibeb/p/6038202.html

原文链接:ABAP Unit Best Practices 

 

2018.04.22更新:现有一个Open SAP的Writing Testable Code for ABAP 视频教程,推荐观看

 

posted @ 2016-11-08 17:21  氢氦  阅读(2874)  评论(0编辑  收藏  举报