Right now, we have hard coded 2 cards: Ice Spirit and Mirror. But we know there are tons of cards that we currently aren't testing.
We could go through each one and hard code these too, but our devs are grabbing a list of cards from the database using REST endpoints.
As Automation Engineers, it is a requirement that we know how to work with the data our devs are using too. Especially in the world of microservices where transactions are happening across multiple calls and services.
There are many pros to this approach, but just think about this one scenario.
If they add a new card with the next release, how would we handle that new card?
Our current implementation would have us create a new card.cs
file that inherits from Card
, but we'd have to find out what the values are for us to hard code them too.
If you think about it, they will be adding that new card to the database, which will get picked up by the endpoints they're already using.
If we also leveraged that same endpoint in our tests, then we wouldn't have to make any code changes because we would already have the new card, as well.
Let's dive right back into code, starting with Postman, to see the endpoint that we'll bring into our framework and tests.
Here we are inside of Postman. You'll see that I have this URL here.
This is the actual card's endpoint, and it is going to be a GET
request.
I don't need to send anything else as part of this request, so I'll just execute it by clicking Send.
And our response is actually a collection or array of card objects.
Now, these card objects are a tiny bit different than ours just because there's an id and an icon value, and we don't currently have that in our Card Base class.
But the great news is that we actually see tons of cards — Royal Ghost, Inferno Tower, Tombstone, Heal — and I'm sure many more.
So, what we want to do is just update our Card class to include the new Id
and Icon
properties, and that's easy enough to do.
Let's switch back over to code, to our Card class and add those 2 properties.
namespace Framework.Models
{
public class Card
{
public virtual string Id { get; set; }
public virtual string Name { get; set; }
public virtual string Icon { get; set; }
public virtual int Cost { get; set; }
public virtual string Rarity { get; set; }
public virtual string Type { get; set; }
public virtual string Arena { get; set; }
}
}
And there we go.
We want to leverage that endpoint in our framework so our tests can use that list of cards.
We'll be creating an API Card Service for this, and our services live inside of our Framework.Services namespace
.
So, right click > New File; we'll call this “ApiCardService.cs.
And then you guys don't need any intro from me.
I think we know the name of this game — public class ApiCardService : ICardService
— and let's make sure to implement our ICardService
as well.
Now, ICardService
is mad at us because we're not implementing everything correctly.
So, let's make sure we implement the interface.
And there is a GetCardByName
.
Right now, we can't actually complete this, so we're going to leave it like that.
But now we're going to get the list of all the cards.
So, we'll say — public IList<Card> GetAllCards()
— there we go.
Now let's make sure we bring in this IList
(Bring in the using statement).
We're going to add a package to our Framework project called Newtonsoft.json.
This is an awesome package to work with JSON to serialize and deserialize. Let's bring that in.
Let's specify the cards API endpoint — public const string CARD_API = “https://statsroyale.com/api/cards”
— it's going to equal that request URL that we had in Postman.
Now what we want to do is to create the client to actually send the request to that URL.
We're going to get a package called RestSharp and add it to Framework. Bring that in, there we go.
Now we can use it — var client = new RestClient(CARD_API)
— and we'll pass in the Cards API. Then let's bring this in as well using RestSharp.
Now we can build our request
.
By default, the method is already GET
, but we're going to explicitly say it here.
And then the format we want to get back is Json.
var client = new RestClient(CARD_API);
var request = new RestRequest
{
Method = Method.GET,
RequestFormat = DataFormat.Json
};
Now let's get the response back and we get the response by sending or executing that request.
So — var response = client.Execute(request)
— that'll send it, give us a response back,
And now we want to check that response.
If the response.StatusCode
is not equal to System.Net.HttpStatusCode.OK
then we want to throw an exception
So, if we get anything that's not “OK”, we're going to throw a System.Exception
here
And let's just say “/cards endpoint failed with”, and then we'll just show them the “response.StatusCode” that we got back
var response = client.Execute(request);
if (response.StatusCode != System.Net.HttpStatusCode.OK)
{
throw new System.Exception("/cards endpoint failed with " + response.StatusCode);
}
There we go.
So, if it gets through this if
statement, we know it's good and now we can convert the Json, we get back into a C# object.
We'll say JsonConvert
and we want to turn that Json String into a C# object. That's called deserializing.
We'll get that Json and turn it into a list of cards and it's just the “response.Content”.
Awesome.
return JsonConvert.DeserializeObject<IList<Card>>(response.Content);
There you go.
It looks like a lot, but really, it's following the same pattern as Postman.
We have our client, you build the request, you send it, and then you get the Json back and do something with it.
So, it's actually pretty linear.
Now we can go back into our GetCardByName
and actually implement this.
So — var cards = GetAllCards()
—and once we have a list of all the cards, now we can get the FirstOrDefaultCard
where the card.Name
is equal to the “cardName” we pass in.
namespace Framework.Services
{
public class ApiCardService : ICardService
{
public const string CARDS_API = "https://statsroyale.com/api/cards";
public IList<Card> GetAllCards()
{
var client = new RestClient(CARDS_API);
var request = new RestRequest
{
Method = Method.GET,
RequestFormat = DataFormat.Json
};
var response = client.Execute(request);
if (response.StatusCode != System.Net.HttpStatusCode.OK)
{
throw new System.Exception("/cards endpoint failed with " + response.StatusCode);
}
return JsonConvert.DeserializeObject<IList<Card>>(response.Content);
}
public Card GetCardByName(string cardName)
{
var cards = GetAllCards();
return cards.FirstOrDefault(card => card.Name == cardName);
}
}
}
There we go.
So now we have an implementation for GetCardByName
and GetAllCards
.
With our API Service complete, we can use it within our existing tests to validate all of the cards instead of the two we just defined.
We'll start by creating that static list first.
This is a list of cards and we'll call this “apiCards” and this is going to be our new ApiCardService
that's going to grab all the cards.
static IList<Card> apiCards = new ApiCardService().GetAllCards();
Then let's make sure we bring in System.Collections.Generic
.
And now we have it, so let’s use it inside of our tests.
The first test we need to add the TestCaseSource
, so let's add that attribute test case source and the source is "apiCards".
Perfect.
Now we can add a parameter to pass into the method and this is of type Card, so we'll say `Card”, we'll call it “card”.
And now instead of getting the card “Ice Spirit”, we can just say card.Name
.
And then change var iceSpirit
to var cardOnPage
.
We’ll change the Assert
as well to “cardOnPage”
Yay! First test done.
Moving on to our second test.
We don't need this anymore [the line of code that fed in the “Ice Spirit” and “Mirror” card names] since we're using apiCards
.
Which means we need to change this TestCaseSource
from “cardNames” to “apiCards”.
And then the parameter is no longer string cardName
, it is a Card so — Card card
.
The cardName
is now card.Name
.
And this one here [the InMemoryCardService
] we don't need any more because we're already passing in the card as an argument.
So now this test is done. Sweet. There you go.
Let's make the first test Parallelizable
so we'll copy this go right here, right under it we'll paste it in.
And then let's also change the category to be cards, since this is our cards suite.
namespace Tests
{
[Parallelizable]
public class CardTests : TestBase
{
static IList<Card> apiCards = new ApiCardService().GetAllCards();
[Test, Category("cards")]
[Parallelizable(ParallelScope.Children)]
[TestCaseSource("apiCards")]
public void Ice_Spirit_is_on_cards_page(Card apiCard)
{
var cardOnPage = Pages.Cards.Goto().GetCardByName(card.Name);
Assert.That(cardOnPage.Displayed);
}
[Test, Category("cards")]
[Parallelizable(ParallelScope.Children)]
[TestCaseSource("apiCards")]
public void Base_Metrics_are_correct_on_Card_Details_page(Card card)
{
Pages.Cards.Goto().GetCardByName(card.Name).Click();
var cardOnPage = Pages.CardDetails.GetBaseCard();
Assert.AreEqual(card.Name, cardOnPage.Name);
Assert.AreEqual(card.Type, cardOnPage.Type);
Assert.AreEqual(card.Arena, cardOnPage.Arena);
Assert.AreEqual(card.Rarity, cardOnPage.Rarity);
}
}
}
And it's a pretty freaking sweet suite if you ask me.
So, open up a terminal and dotnet restore
. We added some packages, so I just want to make sure they restore nicely.
And dotnet build
to make sure things build correctly as well.
Perfect.
And now let's run the tests.
This time we're going to be adding an argument for the number of workers that we want to execute at the same time.
So, we're going to be using NUnit.NumberOfTestWorkers
and this is case-sensitive. You probably want to do 2, 3 or 4 on your personal machine, just because you can't run like 20 at the same time.
dotnet test --filter name=card_is_on_cards_page -- NUnit.NumberOfTestWorkers=4
So, and that's going to be running now.
It's going to throw us over and we're going to see multiple browsers open up. Let me pull another one that went into my second screen. There we go.
Note
You'll notice that things are slower and a little sluggish. I would recommend watching my course Scaling Tests with Docker to see how you can solve that.
But for now, the point is that we'll see the test actually go to different cards now that we didn't have specified before.
“Witch” is now there, “P.E.K.K.A”. is now there… all coming from the API.
For now, I'm going to Ctrl-C a few times just to cancel it because we have a lot of tests now and I don't want to have you watch all of it.
And then the last thing is just to change this name from “Ice_Spirit_is_on_cards_page” to just “Card_is_on_cards_page” since it's more generic now.
I have to explain what's going on because we just did something pretty insane.
As you know, we've only written 2 test methods.
But, and it's a pretty big but, because we are using a TestCaseSource
that is coming directly from the card's API, our 2 test methods now have multiple test cases — 93 to be exact.
There are 93 different cards and we have all of them and are feeding them into our tests.
Now, I would argue that these tests might be better served as lower-level tests. Sure.
But look how easy it was for us to leverage a single endpoint to ramp up almost 200 tests, again with only 2 test methods.
The code we just typed is pretty finger licking good.
Challenge 1
After some test failures, you will see some interesting errors. The biggest one is that "Troop" is not equal to "tid_card_type_character" and "Spell" is not equal to "tid_card_type_spell". The challenge is to solve these errors.
HINT:
tid_card_type_spell
already has the word "spell" in it. Could we use that somehow?
Troop
and character
are the same thing in the context of the game. We should treat characters as troops and vice-versa`
Challenge 2
Similar to Challenge 1, tests will be failing because "Arena 8" is not equal to 8
.
The challenge is to solve this error.
HINTS: