Should I test that?

Should I test CRUD operations?

Verdict

Yes

Test each create, read, update and delete path you write with an integration test against a real database that writes a record, reads it back and checks every field; do not unit test CRUD code against a mocked repository.

Why

Test the CRUD code you write, against a real database. The typical case is a backend with create, read, update and delete endpoints for customers, where your code maps request fields onto an ORM entity. Blast radius is users and Change frequency is regularly, because customers lose data when a write breaks, and resources gain fields with most features. Detectability is eventually, because an update that drops a field returns 200 and a plausible record. Reversibility is with-effort, because damaged rows need a repair script, and Test cost is moderate, because each test needs a local database, so rule R11 gives Test.

When the decision changes
WhenDecisionWhy
The repository only inherits save, findById and delete from a framework base such as Spring Data CrudRepositoryDo not test the inherited methods; test the code that calls themChange frequency falls to rarely, Detectability to immediately and Reversibility to trivial, because the framework's own suite covers these methods
Integration tests already create, read, update and delete each resource with a distinct value in every fieldDo not add unit tests with a mocked repositoryDetectability moves to immediately and Reversibility to trivial, because a broken operation fails those tests in CI before merge
An update or delete changes an invoice amount or an account balanceTest mandatory: the main path, every known failure and the boundary amountsBlast radius rises to money and Reversibility to costly
Read, update and delete load a record by an ID from the URL, and users may touch only their own recordsTest mandatory, including requests for another user's record that must failBlast radius rises to safety-or-legal and Detectability to never, because a missing ownership check raises no error
Only staff edit reference data, through a scaffolded admin such as Django adminDo not test the admin screens; fix a broken form when staff report itBlast radius falls to internal, Change frequency to rarely and Detectability to same-day
Tests can reach the legacy database only through a shared staging copyTest it differently: a nightly query counts records with empty required columns and alerts above zeroTest cost rises to heavy while Detectability stays eventually and Reversibility with-effort

What breaks if you don't test

CRUD code breaks in the mapping between the request and the row, and a mapping break throws no error. A new column reaches the create path but not the update mapper, or an update saves a new object built from the request and empties every field the client did not send. Users notice days later, when a value they saved is gone.

What you lose if you over-test

The common over-test is a unit test per operation with a mocked repository, asserting that create() calls save() once. It passes when the SQL or the column mapping is wrong, and it fails when you rename a repository method. A test that findById() returns a saved row checks Spring Data or Django instead of your code.

How to test

Write integration tests that run each operation through the endpoint or the service, against the database engine production runs. For create, send a distinct value in every field and read the row back. For update, change one field and assert that the others kept their values. For delete, assert that the record and its child rows are gone. Django's test client and Spring Boot's test slices provide the setup. For records that belong to users, add the ownership tests from the OWASP IDOR cheat sheet.

When the answer changes

  • Records have owners, and operations take an ID from the URL.
  • An operation changes money, such as a balance.

Real incident + Code example

The edits that erased gate codes

On a field-service scheduling backend I worked on, the Django view behind PATCH /api/customers/<id>/ built a Customer from the request body with the existing ID and called save(). Django's save() on an object with a primary key updates every column, so fields missing from the body got default values. Our update test sent the web form's full payload and passed. When the Android app added an edit screen that sent only the changed phone number, each edit emptied the customer's access notes, such as gate codes. Technicians met locked gates for 11 days, and we restored 460 notes from nightly backups. The regression test sends one field and checks the others:

import pytest

from customers.models import Customer


@pytest.mark.django_db
def test_patch_keeps_fields_it_did_not_send(client):
    customer = Customer.objects.create(
        name="Ana Ruiz", phone="+34 600 111 222", access_notes="Gate code 4812"
    )

    response = client.patch(
        f"/api/customers/{customer.id}/",
        {"phone": "+34 600 333 444"},
        content_type="application/json",
    )

    assert response.status_code == 200
    customer.refresh_from_db()
    assert customer.phone == "+34 600 333 444"
    assert customer.access_notes == "Gate code 4812"

The fix applies only the fields in the request to the stored record.

FAQ

Should I write unit tests for CRUD operations?

No, test CRUD operations with integration tests against a real database instead of unit tests with a mocked repository. A mocked repository cannot see a wrong column mapping.

Should I write unit tests for CRUD operations when I already have integration tests?

No, integration tests that run each operation with a distinct value in every field already catch what CRUD unit tests would. Unit test only logic inside an operation, such as a price calculation.

Should I test repository save and find methods?

No, do not test the save, find and delete methods that Spring Data or Django provides; their own suites cover them. Test each query you write yourself with one database test.

Should CRUD tests use a real database?

Yes, CRUD tests should use the database engine production runs. SQLite ignores VARCHAR lengths, so a string that PostgreSQL rejects passes a test on SQLite.

Should I test delete operations?

Yes, test each delete you write with one integration test that checks the record and its child rows are gone. For owned records, add a test that deleting another user's record fails.