Assertions in Page Object?!
Photo by Google DeepMind on Unsplash

Assertions in Page Object?!

A few days ago I stumbled on a curious statement while reading some Selenium docs . It said:

Page objects themselves SHOULD NEVER make verifications or assertions. This is part of your test and should always be within the test’s code, never in a page object.

This caused me to feel a wide range of mixed feelings (?) and I decided to dive deeper into it. And this is how this article was born. ??

So… Let’s figure out if we should use POM with:

  1. no assertions, ever?
  2. perhaps some assertions…
  3. yes assertions, always!

Note: this article uses “pseudo-code” examples (JavaScript). Please, do not be triggered by the syntax:)

What is POM?

To be on the same page let’s start with the definition of the “Page Object”! One way of describing it is:

Page Object is a piece of code that knows about all the important stuff on a specific webpage (where the buttons are, where the inputs are, and how to interact with them). You hide all of the complexity (aka “inner html details”) under the class properties and methods. And you call these methods from your tests.

It helps with:

  • ease of writing tests (consistency and less repetition)
  • maintenance speed (Selector changed? Fix it by updating one line of code!)

This definition is too hard and wordy… ?? Let’s simplify it!

It is a DESIGN PATTERN providing a convenient INTERFACE to guide the implementation of tests!

Translation: when creating tests you would type “pagename dot …” and select what you want to do. These are the OPTIONS PROVIDED by the interface. They GUIDE YOU on what you can and cannot do. So that you create more repeatable/reusable/readable (and other Rs) tests!


The assertion confusion

If it is about a convenient interface, why do they say “should never make verifications or assertions”? If we move the assertions to the test it will be more convenient, right?

…right?? ??

Perhaps the confusion comes from the lack of clarity on what an “assertion” is (in the context of tests). Let’s define it:

A test assertion is a logical check that determines the success or failure
Photo by Ivan Krasnoperov on Unsplash

Time for some code!

Let’s try to prove (or disprove) if we should have assertions in the POM? Imagine that we have a “TODO List” webapp. We could create this Page Object class with:

  • addItem() method
  • selector(s) to use inside of the method

class TodoPage {
  #todoItemSelector = '.todo-add-item';
  
  addItem(item) {
    //1. code to add it
    //2. code to validate that it has been added
  } 
}        

Does the “2.” look sus? Well, the purpose of this article is TO SCRUTINIZE it!

Let’s assume it is OK to have it. When we use our Page Object in the test, it will look like so:

it('Add item', () => {
  todo.addItem('Run far')
})        

In the example, the method name is NOT SHOWING that there is a hidden assertion present! Not great... Would it be better to rename it? Make it more explicit?

it('Add item', () => {
  todo.addAndValidateItem('Run far')
})        

Better? Better… But there is still a problem here:

No assertion in the test

…can you even call it a “test”??? Easy fix! Let’s add an assertion to the test (pseudo-code)!

it('Add item', () => {
  todo.addItem('Run far')

  //const item = find by "Run far"
  //expect(item).toExist()
})        

Ha! We fixed one problem and created two more:

  1. Assertion duplication! (one within addItem() and one in the test)
  2. Leaking of the inner “nitty-gritty” details of the HTML into the test

For the first issue, we could clean up the page object Class and remove the validation. It would look like so:

class TodoPage {
  #todoItemSelector = '.todo-add-item';
  
  addItem(item) {
    //1. code to add it
  } 
}        

For the second one, we could add a new method called “checkItemExists()” to the PO class:

class TodoPage {
  #todoItemSelector = '.todo-add-item';
  
  addItem(item) {
    //1. code to add it
  } 
  
  checkItemExists(item) {
    //2. code to validate that it has been added, like:
    //const itemFound = find by 'item'
    //expect(itemFound).toBe(true)
  }
}        

So our test could look like this:

it('Add item', () => {
  const item = 'Run far'
  
  todo.addItem(item)
  todo.checkItemExists(item)
})        

Did we prove it wrong?

From the first glance, it looks like we added the assertion to the Page Object Class and it… looks and feels right!? Does that mean that the people who wrote Selenium docs are wrong?

Not necessarily…

There is an assertion library dependency that has been introduced into the PO Class (expect)

Some people could say this is bad, it violates SOLID (or throw in some other acronyms). Maybe you could rewrite your code to “return the found item” and do an assertion in your test? But then you are decreasing the readability of the test…

On the other hand, you could argue that THE PURPOSE (and responsibility) of the Page Object Class and its methods is to represent the page and what the user can do with it. Surely, the user (or your test) would be able to validate if their item exists on the page! So having the assertion in the method is kind of a “purpose” of the checkItemExists() method. This is literally why it exists!

In my opinion, these types of decisions are a “grey area”, a spectrum of choices. One could go either way and be “sort of” correct. Use your own judgement and pay attention to the consistency of the pattern usage.

  • It could be a PERSONAL CHOICE when starting from scratch
  • and NOT A CHOICE when working with the existing/established pattern

Rotate that arrow! Photo by Google DeepMind on Unsplash

Explicit and implicit assertion

There is another missing piece of the puzzle that we haven’t talked about yet. What if we separated assertions into the subcategories:

  • explicit (important for the test and its readability)
  • implicit (important for test suite maintenance and stability (aka flakiness))

We already have an EXPLICIT ASSERTION in our test (checkItemExists() method), what can we do to add an IMPLICIT one? Perhaps the “network spy” to make sure that the HTTP call “POST /items” API call (to save item) succeeded?

class TodoPage {
  #todoItemSelector = '.todo-add-item';
  
  addItem(item) {
    //1. code to add it
    //2. code to listen to a network call and checking it was a succesful 2XX one
  } 
  
  checkItemExists(item) {
    //3. code to validate that it has been added
  }
}
        

In this code:

  • 1 - is the main action.
  • 2 - technical assertion (implicit). We don’t need it per se, but it might help with flakiness.
  • 3 - main assertion (explicit). We expose it to our test to make it nice and readable.

One more thing

I've seen a few articles defending the “no assertions” stance in POM. They would often quote this article by Martin Fowler. And he also says that “I favor having no assertions in page objects”. What they fail to notice is that he elaborates on it.

First, he mentions that there are 2 camps:

  1. pro-assertions
  2. against-assertions

Second, there is a footnote where he mentions that

One form of assertions IS FINE even for people like me who generally favor a no-assertion style. These assertions are those that check the invariants of a page or the application at this point, rather than specific things that a test is probing.

Translation/adaptation for our “add new item” example:

It is ok to check that the HTML changes (like the table/list that is holding the items now have N+1 row, or checks similar to what we did above with the network calls). But your test SHOULD CHECK that the item has been added and present on the page.

Summary

TLDR:

It is ok to have some assertions

There is no “Page Object Police” that will be coming after you! Also, there are more important problems for you to solve, so don't pay too much attention to things like this one. It is a minor thing in a great sea of problems.

And since you read till the end, I want to say thank you! A reminder that I teach “test automation from scratch” (1-1 lessons) and “all other good stuff” lives behind this link in my profile! ??

You know that I know that you know that you want to click that link ;)


Dmitry Miroshnyk

Quality Assurance Automation Lead

2 个月

You did not mention mostly obvious thing in your article: Let's say you have your assertion for 200 response. Nice. Then you have functional change in the team backlog: item duplicates should not be added and return 400. So what do you have here: 1. You cannot write test using the page as interface: your background check will fail. 2. Of course you can remove background check and write your new test. But... all your previous tests rely on that assertion! You'll make them more flaky (or less readable in case of random failure). 3. You can introduce another method like AddItemWithoutBackgroundCheck() where you duplicate existing code but without verification - but that will decrease readability of your code. And let's imagine you have 20 controls on the page, not 1. Having 20 duplicates definitely isn't the best coding practice.

George Mitchell

Senior Quality Assurance Manager | Director of Quality Assurance | Software QA Manager | Test Manager

2 个月

I use asserts in POM methods to ensure I've arrived at the correct page. Asserts that actually test something unique live in the test case code.

Luke Hill

Cucumber TLC & Ruby Lead | QA Lead | Automation Specialist

2 个月

POM should just be the abstraction layer. Your assertions should be in your runner.

回复
Arkadiusz Frankowski

??? Test Automation Architect & SDET | ??? Passionate Advocate for Advanced Testing Methodologies ??

2 个月

With #CenterTest we keep POMs only as generated from source code classes with getters to elements which can be accessed on the screen. We designed the whole set of different clasees like TextBox or RangeInput which are instantiated in these getters. Then all element logic like click, set or different assertions are available directly from that object.

要查看或添加评论,请登录

社区洞察

其他会员也浏览了