pytest自动测试框架搭建(TestNG的依赖注入详解和使用场景分析)
软件设计方法中的依赖注入是比较晦涩的概念,采用这种方式能够解耦类之间的依赖,提高系统的灵活性。作为当今最为流行的自动化测试框架TestNG,为了增强系统的灵活度,为我们提供了依赖注入的实现,给我们提供了很大的便利。作为TestNG使用者,我们可以不用理解过于晦涩的依赖注入的概念,而很便利的得到由此带来的好处,甚至我们可以不知道这种设计是依赖注入。
本文从依赖注入的概念说起,首先给出依赖注入的简介和入门级示例。之后介绍TestNG的两种依赖注入使用方式:原始依赖注入和外部依赖注入。对于每种方式,都给出简介和示例,同时对何时使用该种方式也做了初步的建议。本文的目的,一方面说明TestNG依赖注入设计的来源和使用方法,从而深入理解TestNG,避免囫囵吞枣的使用,提高遇到问题的解决能力;另一方面,通过实例说明TestNG依赖注入的应用场景,防止理论无法落地实践,毕竟能够解决实际问题的方案才是有价值的。
1 依赖注入简介
1.1 依赖注入简介
详细介绍依赖注入需要很大的篇幅,由于本文不是专门介绍依赖注入,所以这里只做简单的说明。依赖注入,英文是DependencyInjection,简称DI,是软件架构设计中的一种技术。简单理解就是我们定义的类(A)如果需要依赖其他的类(B)来完成工作,通过依赖注入的设计,使得类B的实例化不在类A中进行,而通过第三方的实例化后传递给类A,从而实现解耦的目的。这个第三方就是DI容器,也称DI框架。简单的说就是类之间的依赖关系,不再类之间直接引用,而是通过DI容器进行适配管理。
这样做的好处就是实现解耦,即类A虽然调用了类B,但类A要与类B的实现解耦,这与设计模式中的依赖倒转原则一致:高层模块不依赖于底层模块,他们都应该依赖于抽象,抽象不应该依赖于具体实现,具体实现应该依赖于抽象。
1.2 依赖注入示例
依赖注入的实现方式有很多,常用的有三种方式:构造方法注入(constructor injection),setter方法注入(setter injection),接口注入(interface injection)。下面以构造方法注入作为示例,说明依赖注入的实现机制。
1.2.1 不使用依赖注入
考虑这样的需求,一个测试执行类,在测试完成后需要生成Html格式测试报告,一个简单的实现如下。
HtmlReportService实现如下。
这是没有使用依赖注入的情况的实现。由于在TestRunner类中直接使用了具体的报告实现类HtmlReportService,所以当HtmlReportService发生变化或者需要增加其他类型报告服务时,都需要修改TestRunner代码。两个类的没有解耦。
1.2.2 构造方法依赖注入
对TestRunner类做如下修改,将HtmlReportService的引用修改为对接口的引用,同时不再需要TestRunner本身实例化报告服务,修改为通过构造法方法传递引用。具体代码如下。
HtmlReportService实现变更如下。
IReportService实现如下。
简单的依赖注入组装,可以通过手工的编程方式完成,具体如下。
通过依赖注入的方式,完成了TestRunner与HtmlReportService的解耦,在确保IReportService接口能够兼容旧版本的的情况下,修改HtmlReportService实现,或者增加IReportService实现类,对TestRunner都没有影响。需要修改的是依赖注入的组装过程。
当然可以通过更灵活、强大的第三方框架完成依赖注入的组装,就是上文提到的DI容器。我们平常所说的依赖注入学习,除依赖注入本身的概念和知识之外,更重要的是依赖注入框架的学习。上述例子可以通过Spring框架进行组装。
Beas.xml编写如下。
Spring框架还支持通过注解的方式对依赖注入进行组装,方式更为灵活。
3.3 依赖注入与控制反转
提到依赖注入就不能不提控制反转。控制反转(Inversion of Control,简称IoC),概念也是很晦涩难懂,这里不能详细介绍。简单来说,所谓控制就是使用,反转就是控制权的反转。将使用的对象的控制权,从使用类本身移交给第三方,也就是IOC容器控制,就称为控制反转。
从这个角度来看,依赖注入与控制反转实际是一个概念的不同表述,由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以2004年大师级人物Martin Fowler又给出了一个新的名字:“依赖注入”,相对IoC 而言,“依赖注入”明确描述了“被注入对象依赖IoC容器配置依赖对象”。也有人理解依赖注入是实现控制反转的方法,也是有一定道理的。
2 TestNG中的依赖注入从上述描述中,我们可以看到,依赖注入是一种软件架构的设计方法。对于TestNG来说,TestNG的依赖注入实际是提供了使用该框架的人一种依赖注入的能力,也就是在TestNG框架下能够很容易的实现依赖注入。具体来说,TestNG支持两种依赖注入的方法:原始方法(TestNG框架自身实现的),外部方法(通过类似Guice等依赖注入框架实现的)。
2.1 原始依赖注入方法
2.1.1 原始方法简介
TestNG的原始依赖注入方法,是TestNG框架已经实现的DI容器完成了特定对象的依赖,当使用者需要是,按照特定的方式说明注入即可。TestNG的DI容器会根据使用者的需要将特定对象注入到特定位置。更具体的,当使用TestNG框架管理测试时,可以在方法中添加额外参数,这些参数是我们需要的某个对象的声明。当TestNG发现我们如此配置时,会对该对象赋予正确的实例,供我们使用。这些方法是TestNG注解修饰的方法,这些对象就是需要注入的TestNG内部对象。目前支持依赖注入的方法和对象如下:
1)任何@Before方法或@Test方法都可以定义一个ITestContext参数。
2)任何@AfterMethod方法都可以定义一个ITestResult参数,该参数传递的刚刚运行的测试方法的结果。
3)任何@Before方法和@After方法(@BeforeSuite和@AfterSuite除外)都可以定义一个XmlTest参数,这个参数传递的是当前<test>标签内的信息。
4)任何@BeforeMethod方法和@AfterMethod方法都可以定义一个java.lang.reflect.Method参数。对于@BeforeMethod该参数传递的是即将执行测试的方法,对于@AfterMethod该参数传递的是刚刚运行的测试方法。
5)任何@BeforeMethod方法都可以定义一个Object[]参数。该参数传递的就是即将执行的测试方法的参数列表,这个参数或者是TestNG为我们注入的(就像Method一样),或者是来自@DataProvider的。
6)任何@DataProvider方法都可以定义一个ITestContext参数或java.lang.reflect.
Method参数。后一个参数将接收即将被调用的测试方法。
在方法中使用 @NoInjection注解可以关闭依赖注入。
下表总结了全部可以进行依赖注入的注解修饰方法和相应的参数:
2.1.2 原始方法示例
使用原始方法进行依赖注入非常简单,只要根据上表在期望注入的注解方法中添加相应形参即可。比如下面的例子,在@AfterMethod注解的onAfterMethod中添加了Method,ITestResult,Object[]三个对象,含义就是通知TestNG,该测试类需要在该方法注入如上的三个对象。这样TestNG就是将,这三个对象的实例传递给onAfterMethod,具体这三个对象是如何实例化的,如何维护的,测试类是不需要关心的。当然,TestNG能够确保对象的正确性,比如Method传递的就是刚刚运行的测试方法名称。
2.1.3 场景1:BeforeMethod中使用测试数据
默认的@BeforeMethod给定的onBeforeMethod函数是没有参数的,如果需要在onBeforeMethod函数中对测试环境根据测试数据进行处理时如何实现呢?可以如实现如下的测试类。
利用TestNG的依赖注入机制,在onBeforeMethod声明使用对ITestResult result,Object[] data两个对象的依赖,TestNG会根据上表自动为我们注入这两个类的实例。上述示例中DemoBaseTester为统一读取excel的基类,实现略。编写testng.xml,制造测试数据后,运行结果如下。
2.1.4 场景2:AfterMethod中获取配置文件参数
在AfterMethod中能够获取很多内容,通过默认的ITestResult实例已经可以完成很多功能了,使用TestNG依赖注入ITestContext实例,能获取更过能力。如下示例给出了获取配置文件中parameter定义的内容。
配置文件testng.xml编写如下。
运行结果如下。
2.1.5 其他场景
其他场景与上面的两种情况类似,通过查阅TestNG给定的依赖注入表格,能够知道在每个注解方法下,TestNG提供给我们的注入能力。
具体根据业务逻辑进行选择。比如在BeforeClass中注入ITestContext实例,就可以通过这个接口获取非常多的资源,对逻辑处理、日志打印都是非常有帮助的。
2.2 外部依赖注入方法(Guice)
2.2.1 Guice简介
Guice是Google开发的一个轻量级,基于Java5(主要运用泛型与注释特性)的依赖注入框架(DI)。对比Spring框架,Guice非常小而且快。支持构造方法,属性,方法(包含任意个参数的任意方法,而不仅仅是setter方法)进行注入。Guice采用Java加注解的方式进行托管对象的配置,充分利用IDE编译器的类型安全检查功能和自动重构功能,使得配置的更改也是类型安全的。Guice提供模块对应的抽象module,使得架构和设计的模块概念产物与代码中的module类一一对应,更加便利的组织和梳理模块依赖关系,利于整体应用内部的依赖关系维护,而其他IOC框架是没有对应物的。
2.2.2 Guice示例
依然使用上文的例子进行说明,可以对比一下与Spring框架DI的异同。
首先HtmlReportService和IReportService的实现不变。
TestRunner的实现增加@Inject注解并实现一个新定义的接口IRunner。
IRunner实现如下。
Guice的依赖注入组装需要通过继承AbstractModule父类,并实现configure方法,具体如下。
TestRunnerModule中定义了接口与实现类之间的关系,共Guice后续组装使用。
如下示例展示了如何完成业务逻辑的编程,在这里Guice会通过TestRunnerModule查找依赖的具体实现类,并根据实际业务逻辑的@Inject注解将构造函数中指定的对象进行注入。
综合来看Guice是更轻量的依赖注入框架,能够通过简单的注解和接口实现完成依赖注入的需求。通过Guice在TestNG中进行依赖注入的定制,有3中使用方式,一种比一种更为灵活。
2.2.3 场景1:固定注入
在TestNG中使用Guice进行依赖注入,最简单的方式是通过实现Module接口,固定注入对象。这种场景适用于需要使用依赖注入完成测试类与依赖类的解耦,并且注入的对象时固定的情况。
考虑下面的场景,测试类GuiceTest需要依赖接口ISingleton的实现类GuiceExampleModule完成测试工作,但考虑到后续的扩展性,需要将GuiceTest和GuiceExampleModule通过依赖注入完成解耦。实现GuiceTest如下。
通过@Inject注解通知Guice为我们注入该对象,其中ISingleton实现如下。
这里面涉及到单例的问题,这只是测试类对该接口的实例化要求,与依赖注入本身并无关系。我们可以不考虑这个因素。作为ISingleton接口的实现类ExampleSingleton,实现如下。
通过上面Guice的示例,我们知道为了让Guice正常工作,需要定义接口与实现类的关系,即继承AbstractModule父类,实现configure方法。
与示例不同的是增加了.in(Singleton.class)的调用,就是上文提到的Guice在注入时确保了该实例的单例性。
TestNG配置文件编写如下。
运行结果如下。
在这里隐含着一层含义,Guice的依赖注入组装是没有显示调用的,换句话说是TestNG完成了依赖注入的组装,也就是Guice对象Injector的使用。这也是TestNG说明可以通过Guice完成依赖注入的意义,即TestNG帮助我们完成了Guice的管理。
2.2.4 场景2:灵活注入
上述方式注入的对象是固定的,如果有需要根据情况进行不同对象的注入时,应该如何完成呢?TestNG整合Guice有灵活的方式,还是上述的例子,测试类的Guice注解使用moduleFactory修饰,具体如下。
含义是通过mouduleFactory完成注入类与接口的绑定,这个mouduleFactory是ModuleFactory类,实现如下。
需要实现IModuleFactory接口,具体为createModule函数,函数提供了TestNG的ITestContext对象和当前测试类对象。createModule函数可以自己的业务逻辑,通过这两个对象完成Module的创建。这里的例子,是根据<test>标签的名字完成不同的绑定,当为"Guice-Inject-1"时注入ExampleSingleton1.class,当为"Guice-Inject-2"时注入ExampleSingleton2.class。当然,需要定义这两个module,具体如下。
需要注入的对象ExampleSingleton1和ExampleSingleton2的实现如下。
TestNG配置文件编写如下。
运行结果如下。
工厂类会接收到ITestContext和测试类两个由TestNG初始化好的实例,我们通过实现createModule方法返回一个Guice Module对象的实例,该实例给出了测试类依赖的对象绑定。通过ITestContext和测试类的实例,我们可以获取测试环境的全部信息,比如在testng.xml中定义的参数等。
2.2.5 场景3:注入抽象
TestNG整合Guice除代码层面外,还支持testng.xml中的配置。使用parent-module和guice-stage参数,对<suite>标签进行修饰能够获得更多的扩展性。通过guice-stage的定义我们可以选择创建父Injector的阶段。默认为DEVELOPMENT,可以配置为PRODUCTION或者TOOL。这是Guice对软件运行阶段的定义,根据不同阶段的定义,Guice提供的能力有所不同,TOOL(最小代价,有些功能会无法使用)DEVELOPMENT(快速启动,但不会做校验)PRODUCTION(异常检查与性能,启动会比较慢)。TestNG使用示例如下:
对于如上配置的测试套件,TestNG只会实例化一次该Module。使用这样的配置来获取指定的Guice Module和Module工厂,之后再给创建每个测试类的Injector。使用这种方法,我们可以在parent-module中声明所有公共绑定,也可以在Module和Module工厂中注入在parent-module中声明的绑定。这样就实现了注入的抽象,示例如下。
首先定义parent-module类,实现如下。
其中绑定类MyService和MyServiceImpl实现如下。
其中绑定类MyContext和MyContextImpl实现如下。
之后定义需要被注入的Module。
最后测试类定义如下。
配置文件testng.xml编写如下。
运行结果如下。
可以看到,ParentModule类中定义了MyService和MyContext的绑定。之后MyContext使用构造方法注入的方式,注入到了TestModule中,同时TestModule也定义了MySession的绑定。之后通过testng.xml中的parent-module关键字设置ParentModule类,这样启用了对TestModule的注入。之后的依赖注入使用中,测试类TestClass有两个注入:MyService的注入来源于ParentModule的贡献;MySession的注入来源于TestModule的贡献。这样可以保证所有测试获取的session示例是一致的,同时MyContextImpl对象在测试套件中只创建一次,这样我们可以将该测试套件的全部测试配置在一个公共的测试环境中。
3 总结依赖注入的概念晦涩难懂,但对于TestNG的使用者,我们完全不需要过于关心依赖注入的细节,这也是TestNG提供依赖注入特性的初衷。我们可以不知道什么是依赖注入,就可以很容易的在注解方法内注入我们需要的对象,但前提是这些对象是TestNG承诺在此可以注入的;同时,为了更好的扩展性,TestNG还支持Guice框架进行依赖注入的增强。后者就需要我们对依赖注入有一定的理解了。
总体而言,对于大部分场景,原始的依赖注入已经功能非常强大了,所以学会查阅TestNG原始依赖注入表格是关键。当在测试类之间需要使用依赖注入解耦,就需要增加自定义的依赖注入机制,使用TestNG本身支持的Guice是不错的选择。
本文使用的TestNG版本:6.9.9。
水滴测试公众号输入testng,获取全套代码。
,免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com