Transcripted Summary

In this chapter, we'll implement basic models and services that will help our tests with things like test data.



We'll then use those objects with NUnit to start doing some cool stuff.

In our second test, we have 4 assertions.

That is a valid strategy, but we are working with many different cards that have different properties.

Let's make a third test that validates a different card called Mirror.



I'm going to copy this test because it's going to be almost the exact same test, but we're just going to change the values from Ice Spirit to Mirror.

  • The cardName is “Mirror”
  • The type [category] is “Spell”
  • The arena is “Arena 12”
  • The Mirror cardRarity is “Epic”



If you try the test out, you'll see that it actually passes as expected, but we did copy and paste quite a bit of code.


I think we can do better because there are a lot of cards, and we don't want to copy and paste that many lines of code.

We would have to do that for every single card on that page and there's a ton of them.

Right now those four Assert lines are the biggest offenders. We know the Mirror card isn't going to change anytime soon and if it did, we would want the test to fail.

It's also a Card Object, and we know that Objects can have properties.


So what if we applied the same thing to our card just like we did with the pages?

Let's do it.

We'll start by creating a Models directory in our Framework project.

So inside of here we’ll right click > New Folder, “Models”, and then we'll click and drag “Class1.cs” into “Models”.

Open this up, and we're going to change this [public class] to say instead of "Class1", it's going to be a “MirrorCard”.

Let’s also changed the name of our class [from “Class1.cs” to “MirrorCard.cs”.

We already know the:

  • Name
  • Cost
  • Rarity
  • Type
  • Arena

So let's set those values [for the Mirror card].



We're slowly getting there, but we can't use MirrorCard for the Ice Spirit test

Before we go and make an “IceSpiritCard.cs” file, let's take a look at what we currently have.

I mean each card, at a minimum has quite a few things in common — Mirror and Ice Spirit both have a name, cost, rarity, type, and arena.


# Creating a Card Base class

When objects have things in common, just like the pages, we call them base attributes or base properties.

If you remember, we created a PageBase class that the other pages inherited from. We'll be doing the same thing here, but for cards.

Note

Before I make the Card class, I'm just going to need to change this to be in models [Framework.Models], since that's where there's file lives. And save this.

Now we can make a new file called "Card.cs" inside of our Models directory.

We'll then create the Card class and add the properties that are shared between all of our cards, but this time we'll mark them with the virtual keyword.

This virtual keyword means that classes that inherit this class can override them to give them new functionality or new values.

Now let's have MirrorCard inherit Card.

And you'll notice we get some “yellow squigglies” and that's because MirrorCard and Card have the same names of members.

So all we'll need to do is override.



That's because we want to make sure that MirrorCard’s name is “Mirror”.

Let's add override to the rest of our members here.


# MirrorCard.cs

namespace Framework.Models

{
    public class MirrorCard : Card
    {
        public override string Name { get; set; } = "Mirror";
        public override int Cost { get; set; } = 1;
        public override string Type { get; set; } = "Spell";
        public override string Arena { get; set; } = "Arena 12";
        public override string Rarity { get; set; } = "Epic";
    }
}

Now let's make the Ice Spirit card.

We'll have it inherit from Card base as well.

# IceSpiritCard.cs

namespace Framework.Models
{
    public class IceSpritCard : Card
    {
        public override string Name { get; set; } = "Ice Spirit";
        public override int Cost { get; set; } = 1;
        public override string Rarity { get; set; } = "Common";
        public override string Type { get; set; } = "Troop";
        public override string Arena { get; set; } = "Arena 8";
    }
}

And it'll look just like the Mirror card, but make sure to put in the Ice Spirit values.

We can put it all together to reap the fruits of our labor by creating a method that returns the base card using elements on the page.

In CardDetailsPage, we'll add a GetBaseCard method to get the base card metrics off of the page and return it as a Card Object.

We won't worry about the extra card specific metrics for now.

We create a Base Card Object and give it values.

Then, we return the Card Object.


# CardDetailsPage - GetBaseCard

/// Get the base metrics off the page as a Card object.
public Card GetBaseCard()
{
    var (category, arena) = GetCardCategory();
    return new Card
    {
        Name = Map.CardName.Text,
        Rarity = Map.CardRarity.Text.Split('\n').Last(),
        Type = category,
        Arena = arena
    };
}

Lastly, we'll change our test to use our new method.

So let's go to our CardTests, here in the Mirror test.

We're going to start off by commenting out these 3 [variable] lines, since we're not going to use them anymore.

We're then going to hold a variable called “Card” — this is the card we're going to get off of the details page.

And then we'll also hold a “MirrorCard”, and this is our actual values (Got to bring in framework.models.)


var card = cardDetails.GetBaseCard();
var mirror = new MirrorCard();

Now we'll change our assertions.

  • On the right side, we'll have our card.Name; on the left side we'll have our expected mirror.Name.

  • We'll say mirror.type [instead of “Troop"], and we're replacing it with category. This is why naming is so important because we flipped between category and type. So we're going solidify on “Type” now.

  • For Card Arena, we're going to turn “Arena 12” into mirror.Arena.

  • Then Card Rarity, or just "card.Rarity"; and “Epic” is now mirror.Rarity.


Assert.AreEqual(mirror.Name, card.Name);
Assert.AreEqual(mirror.Type, card.Type);
Assert.AreEqual(mirror.Arena, card.Arena);
Assert.AreEqual(mirror.Rarity, card.Rarity);

Okay, we'll delete these lines [that we commented out]. You don't need it anymore and there we go.



A much cleaner test.

But you guessed it, we're still not done.

# Interfaces and Bases - First Card Service

The last refactor we'll implement is to create a service class that will serve us any type of card we ask for.

Inside a framework we're going to make another folder called "services".

Inside of this, the first thing we're going to do is start with an interface called "ICardService.cs".


Interfaces are rules or contracts that must be followed by any class that implements them.

This is powerful, because it guarantees that multiple classes will have the same method name or signatures.


namespace Framework.Services
{
    public interface ICardService
    {
        Card GetCardByName(string name);
    }
}

This will make more sense later on.


Now let's create our “In Memory Card Service”.

And we're calling it this because this service is going to get us cards that we have defined in our Models directory.

Our namespace is "Framework.Services".

This is a public class, we'll call it InMemoryCardService, and we'll implement the ICardService.

We get errors immediately because we're not adhering to the contract.



So let's fix that, implement interface.

It'll generate it for us automatically and then also include that NotImplementedException. So let's replace this [exception] and put in a switch statement on the card name [switch(cardName)].

The first case is if we pass in “Ice Spirit”, then we would return a new Ice Spirit Card.

The second case is for our “Mirror” and this will return our new Mirror Card.

We'll then add a default case as well, in case someone puts in something incorrect or they pass in “Golem”, we would want to throw a System.ArgumentException.

Let's say something like "Card is not available", and then we'll show them the card that they passed in.

There we go.



We now have our InMemoryCardService class.

# NUnit - [TestCase]

Let's use this in the test.

We'll start by renaming “Card” (using the rename symbol) to “cardOnPage” — just so it's real explicit that this is the card we're getting from the page.

From there, instead of getting the MirrorCard directly, we're going to use our InMemoryCardService to get it instead.

Let's bring it in, Framework.Services.

We'll GetCardByName and pass in “Mirror”.



At this point, test 2 and 3 are now almost identical because of our refactorings — the only difference between them now are the actual names.

This enables us to reuse our test methods by using the test case attributes from the NUnit test framework.

The way that we'll do that is by adding the TestCase attributes right here.

  • So TestCase, and we'll pass in the value “Ice Spirit” — [TestCase(“Ice Spirit”)]

  • For the second TestCase we'll use “Mirror” — [TestCase(“Mirror”)]

We can then leverage these test cases by passing in an argument called cardName inside of the test method.

Now we can replace this hard-coded “Mirror” string with the card name coming from the test case.

Same thing here.



So if we walk through this:

  • The first test is going to have cardName, equal “Ice Spirit”, and then“Ice Spirit”will be injected into these 2 values here and this will become an “Ice Spirit” test.

  • The second time it goes through cardName is going to equal “Mirror”; inject “Mirror” into these 2 values and this will become a “Mirror” test.

That means that this is a lot more generic of a test, so it's no longer tied to just “Mirror”.

So let's make sure we change this [variable for the InMemoryCardService] as well to just say card. And then the test name from “Mirror” to also say “Card”.

Our test method will now work for any card as long as we provide a test case.


However, this is not very scalable because we would still need to write in every test case and make a Model for InMemoryCardService.

# NUnit - [TestCaseSource]

So what we'll do instead is use a TestCaseSource attribute.

We'll start by putting a static array of Strings called “cardNames” — and we'll have this equal our “Ice Spirit, Mirror”.

From there, TestCases are going to be replaced with the TestCaseSource.

And in here we'll pass in the name as a String of our source, which is “cardNames”.




static string[] cardNames = { "Ice Spirit", "Mirror" };

[Test]
[TestCaseSource("cardNames")]

public void Card_headers_are_correct_on_Card_Details_Page(string cardName)
{
    new CardsPage(driver).Goto().GetCardByName(cardName).Click();

    var cardDetails = new CardDetailsPage(driver);
    var cardOnPage = cardDetails.GetBaseCard();
    var card = new InMemoryCardService().GetCardByName(cardName);

    Assert.AreEqual(card.Name, cardOnPage.Name);
    Assert.AreEqual(card.Type, cardOnPage.Type);
    Assert.AreEqual(card.Arena, cardOnPage.Arena);
    Assert.AreEqual(card.Rarity, cardOnPage.Rarity);
}

Save this and feel free to run the tests.

You'll observe that there's actually multiple tasks running and this one for sure is going to have Ice Spirit and Mirror run.

So this test method is now 2 test cases.

# NUnit - [Parallelizable]

By now, you've also noticed that we are running the tests one at a time.


We can greatly improve our test run time by running our tests in parallel.

Now we'll use another NUnit attribute for this.

And for this one we're going to say Parallelizable, and the scope is the children within it — [Parallelizable(ParallelScope.Children)]

Now, we need to use the children just because we have a TestCaseSource.

So that's going to get each one of these 2 tests and split this up, which is pretty cool because now we have a test method that is 2 test cases, and it will run both of them concurrently.

The next thing that we'll add is right here, next to Test we'll say “Category”, and we'll call this one “cards” — [Test, Category("cards")]

Those will make it easy for you to be able to filter your test when you're running them.



So if I open up my terminal here, and we say dotnet test, you can pass in a filter flag and say I want the test category of cards.


dotnet test --filter testcategory=cards

And this will only run tests that have “Category” that equals “cards”.


Once you run the tests, you're going to find out real fast that it actually doesn't work the way we expect.

This is because we're using the same driver across all of our tests.

So even though multiple instances open up, only one of them will go to completion and the other ones will fail.

If we go back to our terminal, we'll actually see that. Yep, sure enough, the other one failed, but it didn't actually close the driver.

We’ll be fixing this in the next chapter by refactoring the WebDriver.



Resources



© 2021 Applitools. All rights reserved. Terms and Conditions Privacy Policy GDPR