📜 ⬆️ ⬇️

Extend Selenium WebDriver. Writing a robot for the RSDN, not thinking about the context

Today I would like to tell you how you can make your PageObject pattern based on Selenium. Yes, I know that they have their own PageObject, but what kind of programmer does not want to write his bike with blackjack and women of easy virtue.

In general, it is very difficult to write automatic tests for the UI - constant problems, something was not loaded there, then the request did not reach and fell on timeout. Who wrote at least a hundred tests - he will understand me. Now imagine that your pages are not just simple HTML, but also contain many different frames and pop-up windows. If you know Selenium well, then you know what that means. Selenium can work simultaneously in the context of a single document, be it a frame, iframe, or a separate modal window.

Once I got the task to write automated tests for a similar project, in which there are a lot of javascript, everything is generated dynamically, a lot of iframes and ajax requests. Having studied Selenium, I began to write tests. After the third dozen tests, I realized that it was not at all easy, as I thought at first. To sculpt the constant SwitchTo () in the test code was simply impossible and the code turned into solid pasta. The logic of the test was completely lost in constant changes of context. In general, I decided to write a small framework for automatic context switching when working with different frames.

All code is written in C #, using NUnit, Autofac, and of course Selenium.
')
Suppose we need to test some actions of an authorized user on the site, and the login form is in the iframe. How this test will look like:

[Test] public void SimpleTest() { var driver = new FirefoxDriver(); //     driver.SwitchTo().Frame("frmName"); driver.FindElement(By.CssSelector("input.login")).SendKeys("my_login"); driver.FindElement(By.CssSelector("input.pass")).SendKeys("my_pass"); driver.SwitchTo().DefaultContent(); //     // ............. //    driver.SwitchTo().Frame("frmName"); driver.FindElement(By.CssSelector("a.logout")).Click(); driver.SwitchTo().DefaultContent(); //  . // ............. driver.Close(); } 

The constant SwitchTo () in each test is lazy, so I, like a true lazy programmer, made sure that our tests looked like this:

 //    var page = _factory.CreatePage<IVacuumPage>(_driver); //    page.Header.Login("my_login", "my_pass"); //   


In my opinion, the benefit is obvious, we separate the grain from the chaff and get clean and most importantly readable tests that the average programmer will understand.

Let's see what is behind this simplicity.

Project structure


Yes, the code in the end turned out not so much. The main thing here is the DomContainer class - this will be the base class for the elements on the logically merged page. In my opinion, rsdn.ru is a very good example where you can easily demonstrate all the advantages of our framework. How PageObject for the site will look like RSDN:

 public interface IRsdnPage : IDomContainer { IRsdnMenuFrame Menu { get; } IRsdnHeaderFrame Header { get; } IRsdnContentFrame Content { get; } } public class RsdnPage : DomContainer, IRsdnPage { public IRsdnMenuFrame Menu { get; private set; } public IRsdnHeaderFrame Header { get; private set; } public IRsdnContentFrame Content { get; private set; } public RsdnPage(IComponentContext context) : base(context) { } //     protected override void Init() { base.Init(); Header = _factory.CreateIframeItem<IRsdnHeaderFrame>(this, Driver.FindElement(By.CssSelector("frame[name='frmTop']"))); Menu = _factory.CreateIframeItem<IRsdnMenuFrame>(this, Driver.FindElement(By.CssSelector("frame[name='frmTree']"))); Content = _factory.CreateIframeItem<IRsdnContentFrame>(this, Driver.FindElement(By.CssSelector("frame[name='frmMain']"))); } } 

We create the structure of the page so that later we don’t think about which element in which frame is located. Once you specify _factory.CreateIframeItem (), then, referring to any element of this object, the framework will automatically switch the context of the driver to the desired frame.

Now let's look at how the login form interface for the RSDN site will look like:

 public interface IRsdnHeaderFrameLoggedOutState : IDomContainer { IWebElementWrapper UserName { get; } IWebElementWrapper Password { get; } IWebElementWrapper LoginButton { get; } void Login(string login, string password); } 

Here, the key is the IWebElementWrapper interface, which repeats the IWebElement interface and adds a bit of the very magic that replaces the context automatically. The prerequisite is that only IWebElementWrapper, and not IWebElement, must be exposed to the outside in order for the whole mechanism to work correctly.

The mechanism itself works very simply and is based on the IInterceptor interface included in the Castle.DynamicProxy assembly. With Autofac, we register the type as follows:

 builder.RegisterType<WebElementWrapper>().As<IWebElementWrapper>() .EnableInterfaceInterceptors().InterceptedBy(typeof(ContextInterceptor)); 

Thus, with such a call pattern:
page.Header.LoggedOutState.LoginButton.Click ();
Our interceptor intercepts the Click () call, goes up the object tree to Header, replaces the context with the required frame, clicks the element in the context of this frame-a, and returns the context back. But in the test code, we will not see this, as it happens automatically. Our goal has been achieved and now all we need to do is to write wrappers for all the necessary elements of the page and use them in our tests, without thinking about the context in which we are located.

Wrappers for a site RSDN


Now I will give an example of a test for the RSDN site, which the following actions do:
  1. Opens the main page
  2. Log in to your account
  3. Go to the search page
  4. We perform a search on the site
  5. Go to the forums page

 [Test] public void FirstTest() { //    var page = _factory.CreatePage<IRsdnPage>(_driver); //    page.Header.Login("***", "***"); //     page.Header.GoToSearch(); page.Content.Reload(); //       Selenium page.Content.Search("Selenium"); //     page.Menu.GoToForums(); } 

In my opinion, simply and concisely. You can create a kind of DSL using Fluent interface for flexibility, and then use the existing pieces in other tests.

To prove that the framework works on large projects, I show a screenshot of our Continuous build:


Here are 310 tests.

For those who are interested to see how it works inside, I laid out the project on the githaba .

If you have any questions, ask them in the comments, I will try to answer.

Source: https://habr.com/ru/post/257049/


All Articles