Challenge #2 is done — here's my solution to Challenge 2 - Data Grid Validation and CRUD Operations
First, the gaps in the user story
1) The delete option didn't include a confirmation dialog before deleting.
2) The translations for:
confirmation dialog title, message, and buttons are not in the attached CSV with all the translations.
error messages for invalid server data.
empty state of the grid after filtering.
success message
3) Behavior for authenticated non-admin users isn't defined (403 page? menu item hidden? redirect?).
The bugs that were fixed now:}
1) Error 500 with duplicated Id: If you call the POST api/server with a duplicated Id (internal id not visible on the UI) you got a 500 error, if the API is used for third party companies they couldn't know if the Id is used or not.

2) Error message isn't clear, for example for name that is longer than 151 characters only sayd StringLenght but the consumer of the API couldn't understand what is wrong.

3) Error message on English. The user selected German but error message like duplicated server key is on English

The fix to the bugs were add a header on the request with the user selected language X-UI-Lang with this header you can get the error messaged on different languages
API Testing
In general I created the tests:
- Servers_WithAdminUser_ReturnsServers: Test the GET api/Server to check that returns at least one server and valid the json schema on the response.
- Servers_withNormalUser_Returns403: Test the GET api/Server to check that returns Forbidden (403) when the user is not admin.
- Servers_withoutJwt_Returns401: Test the GET api/Server without JWT token returns 401. In some projects this validation was part of integration test by developers and not by the QA. So I only cread one as example but you can also create for the other POST/PUT/DELETE
- Servers_WithValidServerInfo_CreatesAServer: I tested the POST api/Server with valid random data and checked the response returns the same info
- Server_WithValidKey_ReturnsServerInfo: GET api/Server/:serverKey to test that returns the info for the created server. You can include this also on the previous test but I prefer add another test case
- Server_WithInValidKey_Returns404: GET api/Server/0 (0 - Is invalid key due to min value is 1) returns 404, sometimes instead of 404 some API returns 200 with empty value
- Servers_WithInvalidInfo_ReturnsError: I tested with duplicated key, missing required fields, a name with more than maximumn lenght (151) characters and I tested the error message is clear and translated to the user language on the header of the request.
- Servers_WithValidServerInfo_UpdatesAServer: PUT api/Server/:serverKey with valid info returns status code 204 (No content)
- Servers_WithInvalidServerId_returns400Error: Test with invalid key, or invalid json like string instead of number
- Servers_WithValidServerId_DeletesCorrectly: Test the DELETE api/Server/:serverId with the created serverId will delete the created Server
- Servers_WithInValidServerId_Returns404: Test the DELETE api/Server/:serverId with serverId as invalid server id like 0 should returns 404
Postman testing
For API testing, I include the header in requests in Postman. You can use a .csv file with the same name as your environment collections to replace the error message for different languages.
I updated the utils.CheckRequired function to validate the field and the custom error message that previously was only Required.
I added a Pre-request script in the servers folder with code to log in as an admin user and set the Bearer token. With this approach, when you make any request in the Servers folder, you have a valid token. If not, you first need to run the admin login request to get a valid token.
// Login as Admin and store the AccessToken
const loginUrl = pm.environment.get("AuthAPI") + "/api/users/login";
const loginBody = {
Company: pm.environment.get("company"),
UserName: pm.environment.get("userNameAdmin"),
Password: pm.environment.get("passwordAdmin"),
Language: pm.environment.get("language")
};
pm.sendRequest({
url: loginUrl,
method: "POST",
header: { "Content-Type": "application/json" },
body: {
mode: "raw",
raw: JSON.stringify(loginBody)
}
}, function (err, response) {
if (err) {
console.error("Login failed:", err);
} else {
const jsonData = response.json();
pm.collectionVariables.set("AccessTokenAdmin", jsonData.AccessToken);
console.log("AccessTokenAdmin set successfully");
}
});
Enter fullscreen mode Exit fullscreen mode
- To test the POST I created a collection variables with random data. Sometimes if the user for example is id sometimes the testing user ids start with a long number like 999,0000 there are different random values that you can use on postman you can see the full list
const randomName = pm.variables.replaceIn('{{$randomProductName}}');
const randomUrl = pm.variables.replaceIn('{{$randomUrl}}');
const randomKey = Math.floor(Math.random() * (999 - 2 + 1)) + 2;
pm.collectionVariables.set("serverKey", randomKey);
pm.collectionVariables.set("serverName", randomName);
pm.collectionVariables.set("serverUrl", randomUrl);
Enter fullscreen mode Exit fullscreen mode
And I checked the server values in the Post-response
const response = pm.response.json();
utils.StatusCreated();
const serverKey = pm.collectionVariables.get("serverKey")
const serverName = pm.collectionVariables.get("serverName")
const serverUrl = pm.collectionVariables.get("serverUrl")
pm.test("Server key is: " + serverKey, function() {
pm.expect(response.Key).to.eq(serverKey)
})
pm.test("Server name is: " + serverName, function() {
pm.expect(response.Name).to.eq(serverName)
})
pm.test("Server Url is: " + serverUrl, function() {
pm.expect(response.Url).to.eq(serverUrl)
})
pm.test("Server Active is: true", function() {
pm.expect(response.Active).to.be.true;
})
pm.collectionVariables.set("serverId", response.Id)
Enter fullscreen mode Exit fullscreen mode
If you want to validate the JSON Schema of the response you can use the jsonschema function
const schema = {
type: "array",
minItems: 1,
additionalProperties: false,
items: {
required: ["Id","Key", "Name", "Url", "Active"],
properties: {
"Id": {
"type": "integer"
},
"Key": {
"type": "integer"
},
"Name": {
"type": "string"
},
"Url": {
"type": "string",
"format": "uri",
"qt-uri-protocols": [
"https",
"http"
]
},
"Active": {
"type": "boolean"
}
}
}
}
// Test that the status code is 200
utils.StatusOk();
// Compare the jsonSchema
pm.test("Valid schema", ()=>{
pm.response.to.have.jsonSchema(schema)
})
// Parse the response body as a JSON object to get the first Server
const jsonData = pm.response.json();
const server = jsonData[0]
pm.collectionVariables.set("serverKey", server.Key);
Enter fullscreen mode Exit fullscreen mode
Restsharp
For RestSharp, sometimes I have a class to generate test data, for example, with a prefix like "test" and to get random data with a date and time as part of the name.
I created a Data folder with a ServerDataClass with a random server key that is used on name and url
using APINet.Models;
namespace APINet.Data;
public static class ServerTestData
{
public static Server RandomServer()
{
var key = Random.Shared.Next(999_000, 999_999);
return new Server
{
Key = key,
Name = $"Server {key}",
Url = $"https://example-{key}.contoso.com",
Active = true
};
}
}
Enter fullscreen mode Exit fullscreen mode
This folder is shared for both projects RestAssured and RestSharp
Also I created the server and with try and catch I deleted the created Server. I used this approach when I don't want to add a lot of testing data to the testing environments. Sometimes I tested on production with some testing account and with some SQL scripts you can delete the testing info.
RestSharp example
[Test]
public async Task UpdateServer_WithMismatchedId_Returns400()
{
var client = ApiClient.Create(Configuration, AuthToken, useAuthUrl: true);
var created = await CreateServerAsync(client, ServerTestData.RandomServer());
try
{
var mismatched = new Server
{
Id = created.Id + 999_999,
Key = created.Key,
Name = created.Name,
Url = created.Url,
Active = true
};
var response = await client.PutAsync($"/api/Server/{created.Id}", mismatched);
await Assert.That(response.StatusCode).IsEqualTo(400);
}
finally
{
await client.DeleteAsync($"/api/Server/{created.Id}");
}
}
Enter fullscreen mode Exit fullscreen mode
RestAssured Example
[Test]
public void UpdateServer_WithMismatchedId_Returns400()
{
var created = CreateServer(RandomServer());
try
{
var mismatched = new Server
{
Id = created.Id + 999_999,
Key = created.Key,
Name = created.Name,
Url = created.Url,
Active = true
};
Given()
.Header("Authorization", $"Bearer {AuthToken}")
.ContentType("application/json")
.Body(mismatched)
.When()
.Put($"{AuthUrl}/{ServersEndpoint}/{created.Id}")
.Then()
.StatusCode(400);
}
finally
{
DeleteServer(created.Id);
}
}
Enter fullscreen mode Exit fullscreen mode
E2E Testing
For the playwright framework I created componentes for:
- Grid: To check headers, click on edit, delete button
- Dialog: Now I only have the confirm delete dialog but on real projects I tested different dialogs not only for delete. So I created a dialog component with functions to validate the dialog title, message and buttons
I usually prefer the tests can be executed on parallel and independent so for edit a server first I created a server with API, I filtered because the server could be on the second page, and after I edit with UI and at the end I deleted the created server. }
I didn't include sorting automation testing because sorting was only on the UI. Sometimes sorting is tested with unit tests or at the component level, but I also created some tests that test sorting, and the bugs I found were related to sorting date columns. But I can include on next challenge related to test the export to Excel and PDF
You can check the repo with the solution. As a plus I'm adding claude skills integration to generate the manual test cases, playwright test and I will include the API tests.
Thanks for following along with my take on Challenge #2! Testing is all about continuous learning, so don't hesitate to ask questions or share your feedback below. If this helped you in any way, feel free to share it with the community.
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.