TestingAPIs is still being built. If something breaks, email support@testingapis.com.

Testing mutations

Creating, changing and deleting data, and proving each change happened.

Queries read. Mutations write. On this API you can create, update and delete users, and add, toggle and delete todos. Writes are saved and visible to everyone using the API, so the records you create in this lesson are real until someone deletes them. Delete yours when you're done.

If you've done testing a resource end to endin the REST tutorial, the method is the same: a response is a claim, and only a separate read proves what was stored. What's different is how GraphQL reports the outcome.

Creating a record

A mutation looks like a query that starts with mutation instead of query. It takes arguments, and it has a selection set, because a mutation returns data like any other field:

Mutation
mutation CreateUser {
  createUser(name: "Kofi Mensah", job: "QA Engineer", city: "Accra") {
    id
    name
    job
    city
  }
}
Response (200 OK)
{
  "data": {
    "createUser": {
      "id": 3543,
      "name": "Kofi Mensah",
      "job": "QA Engineer",
      "city": "Accra"
    }
  }
}

Your ID will differ. IDs for new users on this API aren't sequential, so don't write a test that expects the next number up. The selection set decides what comes back, and asking for every field you sent is the cheapest check you can make: if city comes back empty, the mutation lost it.

Then read it back with a plain query, in a separate request:

Query
query GetUser($id: Int!) {
  user(id: $id) {
    id
    name
    job
    city
  }
}

Compare every field with what you sent, and check that the new ID appears in the { users { id } }list too. On this API it does. On APIs with a cache or a search index in front of the list, it sometimes doesn't, and that's a bug worth finding before users do.

Updating

updateUser takes the ID and all three fields, and all of them are required:

Mutation
mutation {
  updateUser(id: 3543, name: "Kofi Mensah", job: "Test Lead", city: "Accra") {
    id
    job
  }
}

Because every field is required, it behaves like a REST PUT: you send the whole record each time. There's no way to change only the job without also sending the name and city, so a client that sends an empty name by mistake will wipe it. Many GraphQL APIs take an input object with optional fields instead, which behaves more like PATCH. Find out which kind you have, then test the case where a field is left out or sent as null.

When the record doesn't exist

A REST API answers a request for a missing record with 404. GraphQL has no standard for this, and this API shows how much room that leaves. Every mutation below was sent with an ID that doesn't exist. All of them returned 200, and none returned an error:

MutationReturn typeWhat came back
updateUser(id: 9999, …)Usernull
toggleTodo(id: 99999)Todonull
deleteTodo(id: 99999)Boolean!false
deleteUser(id: 9999)String!"User 9999 not found"

You can send the last one safely, since there's nothing to delete:

POST/graphql
{"query":"mutation { deleteUser(id: 9999) }"}
This request uses an endpoint for signed-in members.Sign inCreate a free account

Three different ways to say "not found" in four mutations. The last one is the hardest to work with. A successful delete returns "User 3543 deleted successfully", so the only way a client can tell success from failure is by reading English text. If anyone ever rewords the message, every client that checks it breaks, and no schema diff will warn them.

This kind of inconsistency is common in GraphQL APIs, because each mutation is designed on its own. Writing the table above for your API is a quick piece of work with a high chance of turning up something to discuss. Common designs that hold up better are returning null plus an error, or returning a result object with explicit fields such as success and message.

Bad input

The schema catches wrong types and missing arguments. Everything else is up to the server code, and that's where to look. Two of this API's mutations check their input; the user ones don't:

Mutation and response
mutation {
  createUser(name: "", job: "", city: "") { id name }
}

{ "data": { "createUser": { "id": 3854, "name": "" } } }

A user with no name, no job and no city, saved and given an ID. The schema says String!, and an empty string is a string, so GraphQL is satisfied. Compare addTodo, which rejects an empty title with Field 'title' is required.and a priority outside high, medium and low. It doesn't check everything, though:

Mutation and response
mutation {
  addTodo(title: "Orphan", userId: 99999, priority: "low") { id userId title }
}

{ "data": { "addTodo": { "id": 31, "userId": 99999, "title": "Orphan" } } }

User 99999 doesn't exist, and the todo was created anyway. In a real system that's a record nobody can see in their list, belonging to nobody. Things to try on any create mutation:

  • Empty strings for every required string argument.
  • Very long strings: a 10,000-character name, say.
  • References to records that don't exist, like the user ID above.
  • References to records that belong to someone else, if the API has accounts.
  • Leading and trailing spaces, and whether they're trimmed.
  • The same create sent twice. Does it make two records, and should it?

Several mutations in one request

A document can contain several mutation fields. Use aliases when they call the same mutation:

Mutation
mutation TwoUsers {
  first: createUser(name: "Kofi Mensah", job: "QA Engineer", city: "Accra") { id }
  second: createUser(name: "Lena Vogel", job: "Developer", city: "Berlin") { id }
}
Response
{
  "data": {
    "first": { "id": 3543 },
    "second": { "id": 4531 }
  }
}

Query fields may run in any order, even in parallel. Mutation fields at the top level must run one after another, in the order written. That's in the GraphQL specification, so a client can create something and then change it in the same request. What the specification does notpromise is that they succeed or fail together. There's no transaction. If the second one fails, the first one has already happened.

Clean up after yourself

Tests that create data should delete it, especially on a shared API like this one. Delete in the reverse order you created, check that each delete reports success, and then check with a query that the record is really gone:

  1. Create the record and keep its ID.
  2. Run your checks.
  3. Delete it, and check the delete's return value.
  4. Query it again and expect null.
  5. Delete it a second time and check you get the not-found answer from the table above.

Exercise

Add a todo with addTodo. Toggle it twice with toggleTodo, reading completed after each toggle. Is toggling twice the same as not toggling at all? Then delete it with deleteTodo, delete it again, and query the todo list to make sure it's gone. Write down every response, then say which of them a client could misread.

Practise and go further