很早以前就想搞自动化Web功能测试,知道了Selenium,看了些文档,但当真正到开发项目的关头,测试总是草草而过,跑完一遍手工的拉倒,回归测试更是无从谈起。前几天终于痛下决心写起使用Selenium的Web自动测试代码。
先扯扯Selenium(字面上是“硒”的意思)。当初出来的时候结结实实把我震撼了一回。原来搞Web自动化测试基本上走的是GUI的那条老路(当然可能也是我当年孤陋寡闻),而这种GUI自动测试工具往往是功能强大的私有软件(比如WinRunner),另外对Web这种多变的测试元素用起来也是很别扭。Selenium另辟蹊径,从JS入手调用浏览器,同时允许通过跨平台的代码调用。从API就可以看出来这个东西的直观易用:
1: selenium.windowMaximize();
2: selenium.click("link=信息系统");
3: selenium.waitForPageToLoad("30000");
4: selenium.click("//a[contains(@href, 'projects.do?method:view&project.id=1')]");
5: selenium.waitForPageToLoad("30000");
另外,Selenium还提供了一个Firefox插件-Selenium IDE,用于录制用户的操作(虽然部分动作无法录制)。录制的动作可以直接导出成HTML/Java/Ruby/C#/PHP等格式的代码,配合提供的SeleneseTestCase,当作JUnit的TestCase使用。
不过这折腾就折腾在这TestCase上。Selenium的开发提供的SeleneseTestCase是Junit3风格的,放在JUnit4底下跑,JUnit4的Annotation功能就用不起来了(这点经我浏览代码查证)。Selenium要启动浏览器,如果用不上@BeforeClass的话,每次启动都初始化一下Selenium,开个IE或者Firefox,这个测试的效率可吃不消(也有比较麻烦的Workaround,但总觉得不是很好)。而甩开SeleneseTestCase的话,又舍不得那个在测试没有通过的时候自动截屏的功能。于是开始Google,兼看Junit4的源代码。
最后终于在这里找到了方案。但问题又来了:Eclipse自带的Junit 4.3还没有这个方案需要的类(JUnit4ClassRunner )!自己手动把原来的Library移除换上Junit 4.5,又发现这个类才用了一个版本就Deprecated了。于是换上了新类(BlockJUnit4ClassRunner)。
接下去的工作就是用上Decorator模式,把原来SeleneseTestCase的代码给移到新的BaseTestCase上。期间还遇上了一些Override的问题。上代码:
SeleniumTestListner类,用于拦截异常的抛出
1: public class SeleniumTestListener extends RunListener {
2: private SeleneseTestCaseAdapter stca;
3: @Override
4: public void testFailure(Failure failure) throws Exception{
5: Selenium selenium = stca.getSeleniumTestBase().getSelenium();
6: if(!stca.isCaptureScreenShotOnFailure()){
7: return;
8: }
9: if (selenium != null) {
10: String filename = failure.getDescription().getDisplayName() + ".png";
11: try {
12: selenium.captureScreenshot(filename);
13: System.err.println("Saved screenshot " + filename);
14: } catch (Exception e) {
15: System.err.println("Couldn't save screenshot " + filename + ": " + e.getMessage());
16: e.printStackTrace();
17: }
18: }
19:
20: }
21:
22: public void setSeleneseTestCaseAdapter(SeleneseTestCaseAdapter stca){
23: this.stca = stca;
24: }
25: }
SeleniumTestRunner类:加入SeleniumTestListener监听器,得到Test实例并注入监听器
1: public class SeleniumTestRunner extends BlockJUnit4ClassRunner {
2: private SeleniumTestListener stl;
3: public SeleniumTestRunner(Class<?> c) throws Exception{
4: super(c);
5: stl = new SeleniumTestListener();
6: }
7:
8: @Override
9: public void run(RunNotifier rn){
10: rn.addListener(stl);
11: super.run(rn);
12: }
13:
14: /**
15: * Copy from BlockJUnit4ClassRunner.methodBlock(FrameworkMethod method)
16: * to get tested instance
17: * @author Marshall
18: */
19: @Override
20: protected Statement methodBlock(FrameworkMethod method) {
21: Object test;
22: try {
23: test= new ReflectiveCallable() {
24: @Override
25: protected Object runReflectiveCall() throws Throwable {
26: return createTest();
27: }
28: }.run();
29: } catch (Throwable e) {
30: return new Fail(e);
31: }
32:
33: //Marshall added
34: stl.setSeleneseTestCaseAdapter((SeleneseTestCaseAdapter)test);
35:
36: Statement statement= methodInvoker(method, test);
37: statement= possiblyExpectingExceptions(method, test, statement);
38: statement= withPotentialTimeout(method, test, statement);
39: statement= withBefores(method, test, statement);
40: statement= withAfters(method, test, statement);
41: return statement;
42: }
43: }
SeleniumTestCaseAdapter, 打上了@RunWith。所有的TestCase都继承这个Adapter。但这个Adapter并不继承JUnit的TestCase类
1: /**
2: * Decorator pattern which makes this class have the same capability as the
3: * SeleneseTestCase class had provided. Copy a lot of source code from the
4: * decorated class.
5: * @author Marshall
6: */
7: @RunWith(SeleniumTestRunner.class)
8: public class SeleneseTestCaseAdapter {
9: private static SeleniumTestBase stb = new SeleniumTestBase();
10: private boolean isCaptureScreenShotOnFailure = false;
11:
12: /** Use this object to run all of your selenium tests */
13: protected static Selenium selenium;
14:
15: @BeforeClass
16: public static void setUpSelenium() throws Exception{
17: stb.setUp("http://127.0.0.1:8080/", "*iexplore");
18: selenium = stb.getSelenium();
19: }
20:
21: @AfterClass
22: public static void tearDownSelenium() throws Exception{
23: stb.tearDown();
24: }
25:
26: ......
27: }
样例测试类:
1: public class TestRiskRepo extends SeleneseTestCaseAdapter {
2: public TestRiskRepo(){
3: setCaptureScreenShotOnFailure(true);
4: }
5: @Before
6: public void set() throws Exception {
7: selenium.open("/apis/login.do");
8: selenium.type("j_username", "marshall");
9: selenium.type("j_password", "xxxx");
10: selenium.click("//input[@value='登录']");
11: selenium.waitForPageToLoad("30000");
12: }
13:
14: @Test
15: public void repo() throws Exception {
16: selenium.windowMaximize();
17: selenium.click("link=信息系统");
18: selenium.waitForPageToLoad("30000");
19: selenium.click("//a[contains(@href, 'projects.do?method:view&project.id=1')]");
20: selenium.waitForPageToLoad("30000");
21: verifyTrue(selenium.isTextPresent("xxx"));
22: selenium.click("link=组织风险库");
23: selenium.waitForPageToLoad("30000");
24: verifyEquals("组织风险库 | APIS", selenium.getTitle());
25: verifyTrue(selenium.isTextPresent("可能性"));
26: verifyFalse(selenium.isVisible("//button[contains(text(), '搜索')]"));
27: selenium.click("css=.x-tool");
28: verifyTrue(selenium.isVisible("//button[contains(text(), '搜索')]"));
29: }
30:
31: }