1

There's a question that struggles me for a long time already and so far I couldn't find or figure out a good answer. Hopefully, you will help me with that, folks.

Before the story, a little background: in test automation, there's one very popular pattern called Page Object Model. Briefly speaking, it's about mapping all essential elements of a single web page on class properties and developing corresponding methods to interact with the page. It looks like this (TypeScript syntax):

class LoginPage {
  private driver: WebDriver;

  private phoneNumberInput = '[data-test="phone-number"]'

  private passwordInput = '[data-test="password"]'

  private submitButton = '[data-test="submit"]'

  constructor(driver: WebDriver) {
    this.driver = driver;
  }

  async typePhoneNumber(value: string) {
    await this.driver.type(this.phoneNumberInput, value);
  }

  async typePassowrd(value: string) {
    await this.driver.type(this.passwordInput, value);
  }

  async submit() {
    await this.driver.click(this.submitButton);
  }

  async loginAs(phoneNumber: string, password: string) {
    await this.typePhoneNumber(phoneNumber);
    await this.typePassowrd(password);

    await this.submit();
  }
}

So, we have a class that represents a login page with three CSS locators mapped on private properties, four public methods to perform some actions, and eventually, we have a private property driver which is an object that has all the methods allowing to interact with the actual web page.

Easy peasy.

And what's my problem here? Personally, I'm not convinced if driver should be a class member of LoginPage, because how this property describes the class? What in common they have with each other? An object of type Webdriver isn't a part of LoginPage in the same way as phoneNumberInput, is it? I feel it'd be "more correct" to pass a driver to each method instead:

class LoginPage {
  (...)

  async typePhoneNumber(driver: WebDriver, value: string) {
    await driver.type(this.phoneNumberInput, value);
  }

  async typePassowrd(driver: WebDriver, value: string) {
    await driver.type(this.passwordInput, value);
  }

  async submit(driver) {
    await driver.click(this.submitButton);
  }

  async loginAs(driver: WebDriver, phoneNumber: string, password: string) {
    await this.typePhoneNumber(driver, phoneNumber);
    await this.typePassowrd(driver, password);

    await this.submit(driver);
  }
}

But even if it seems like the valid approach it makes everything worse. It's simply easier to pass an instance of WebDriver to the constructor and get over with that.

So, how do think, what's the golden mean here between correctness and convenience? Should the class members always describe the class or there some exceptions from the rules like services?

2
  • "It's simply easier to pass an instance of WebDriver to the constructor and get over with that." — how else would you access this object in the instance methods? – Greg Burghardt May 21 at 22:27
  • There are plenty of ways. We can pass the object to methods as I showed in the example. We can have a static class member of WebDriver type and initialize it outside the constructor but it's not the point here, I guess – ourgraciousruler May 22 at 20:53
2

I'm going to try to shift your thinking here. People take the notion that OOP is modeling "real-world objects" too literally. If you think about it, the decision that a web driver (which, abstractly speaking, could even be a human) is not a part of the concept of an interactive web page, is, in some sense, arbitrary. When you choose object properties to place in the class, you're already making decisions about what to include and what not (e.g. you included the CSS selector for the submit button, but not the width and height of that button). Same thing with methods. The idea of object modeling is to be more conscious about those choices.

It's not about representing some object in some way that just happens to pop into your mind, and it's not just about representing things, it's also about what those things do. Remember, what we are about is making the computer do things (and this is true whether you're doing OOP or manipulating pure data).

The idea is for your model to capture the behaviors relevant to the application and to the code that's going to use them (in this case, your test code). The Page Object isn't representing "a page" in some detached way, without any context associated with it - it's explicitly representing a test-specific abstraction of a page that can be driven. It's explicitly an object that manipulates HTML elements of a specific view in an abstract way, so that your tests are not brittle (assuming that the set of the behaviors of the page has stabilized). It's a page manipulator that takes in a driver as a dependency. (And in fact, it likely doesn't even represent the page as a whole, just some interactive part of it).

Granted, the name LoginPage doesn't explicitly reflect this, but it's probably fine to leave it like that if the general sentiment in the team is that that naming convention is not confusing; as long as everyone with vested interest in reading the code (so, working on the project) is on the same page (pun totally intended1). The naming convention is up to you and your team.

So in that sense, what you have is not incorrect, conceptually. Furthermore, in OOP, when it comes to objects that are not behaviorless data structures, you ideally don't even know what data your object stores internally; your object is characterized through the behaviors it provides. It's almost like a small computer that does something specific for you. So in terms of representation, whether you pass the driver through a constructor or through every method doesn't make that much of a difference. But it makes a difference in other ways. If you take the stance that the internal data of the object is private (invisible to outside code, not part of the object's "API"), both variations are pretty similar. In both cases you have a dependency on the driver; the object provides behaviors that are parameterized by the driver. What's different is that constructor injection implies that you pass the driver once, and then you have an instance configured to work with that driver for the duration of its lifetime, while method injection implies that the object is designed to potentially accept a different (subtype of the) driver on every call.


1 I feel that nowadays, when someone says "pun not intended", the pun was very much intended 99% of the time.

4
  • I've read it carefully and it's the answer that I was looking for. I don't know the concept of OOP so much as you noticed probably. I knew I was missing something. Filip, just to make it crystal clear, you're saying there's anything wrong in having an instance of WebDriver as a class member of page object? I know your point wasn't to say "this way is good and this one isn't", however, I just want to confirm I understood you correctly. Btw, could you recommend any books or other material to learn more about OOP? – ourgraciousruler May 22 at 21:02
  • @ourgraciousruler - Yes, I'm saying that it's fine to have the WebDriver as a member in this context. But the other point I want to make is, when you are creating software, there are always aspects that are unique to the problem domain you're working on. This means that nobody can tell you what the 100% best way to do something is, because they are not working on your project. In some sense, you, as a software engineer, are exploring new ground; there will be things there with no prescribed solutions. It's not all "following the book", some of it is adding new entries to it. 1/3 – Filip Milovanović May 23 at 18:14
  • So, even though we say "best practices", all the recomendations are really "good practices" that generally work, but that you shouldn't be bound to. So don't think in terms of absolute right or wrong, but rather try to understand the reasoning behind these reccomendations, and think in terms of "Does this make sense for what I'm doing?" As for books, maybe a good way to start would be by exploring ideas here, and here 2/3 – Filip Milovanović May 23 at 18:14
  • These, I think, are good because they aren't too dense or academic, but should help introduce some of the deeper OOP/design concepts, and I think it helps to see how these concepts manifest across languages (one uses Ruby, the other C#). To me, OO is at its core a design technique, so there's, on one hand, learning/understanding the underlying principles, and on the other, there's learning how to make use of them in a specific language within the constraints it imposes. Note also that I'm not saying that everything should be OO; you can combine OO with other approaches. 3/3 – Filip Milovanović May 23 at 18:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.