Mocking Cloud-Optimized GeoTIFF Reads in Tests
Code that reads Cloud-Optimized GeoTIFFs (COGs) from object storage is slow and non-deterministic to test against the real thing — network latency, credentials, and remote state all leak into the suite. This guide sits beneath raster mocking techniques and shows how to build in-memory COG fixtures with rasterio’s MemoryFile, exercise windowed reads and overview levels without a network, and assert that your reader requests only the bytes it needs. The specific value of mocking a COG rather than any GeoTIFF is that the COG’s whole point is partial, range-request reads — so a test must prove the reader uses windows and overviews, not that it can read a file at all.
Why real COG reads make bad tests
A COG served from S3 or GCS is read via HTTP range requests: the reader fetches the header, then only the tiles and overview levels a query touches. Testing against the live object couples the suite to network availability, credentials, and egress cost, and makes timing non-deterministic. Worse, it hides the property you actually want to verify — that your code reads a small window rather than pulling the whole raster — because a slow full read and a fast windowed read both “pass” against a real file. An in-memory fixture makes the read local, deterministic and inspectable, so the windowing behaviour becomes assertable.
What makes a COG different, and why that matters for the mock
A Cloud-Optimized GeoTIFF is an ordinary GeoTIFF with two constraints that exist so a reader can fetch part of it over HTTP: the data is tiled rather than striped, and the metadata header sits at the front of the file. Together those let a client read the header in one request and then request only the byte ranges covering the tiles it needs.
That structure is the whole subject of the test. A mock that reproduces the pixels but not the tiling proves nothing about the access pattern, because a striped file forces a reader to pull whole rows — which is precisely the behaviour the COG format exists to avoid. If the test is about what gets fetched, the mock has to be genuinely tiled and genuinely have its header in front.
What the overview pyramid buys, and how to fixture it
The second half of the COG contract is the overview pyramid: reduced-resolution copies stored in the same file so a reader asking for a coarse view fetches a small image instead of downsampling a large one. A fixture without overviews cannot test whether your reader picks the right level, which is usually the single largest determinant of how much data a map request moves.
Mocking-approach reference
| Need | Technique | What it proves |
|---|---|---|
| Local COG bytes | rasterio.MemoryFile |
Read logic without a file on disk |
| Overview levels | build with overview_level / factors |
Reader picks the right resolution |
| Windowed read | Window + assert shape |
Only a sub-region is read |
| Range requests | mock the HTTP session | Reader fetches ranges, not the whole object |
| Deterministic pixels | fixed NumPy array | Reproducible assertions |
Step-by-step implementation
The pattern targets rasterio 1.3+ and NumPy, building an in-memory COG and asserting windowed reads.
Step 1 — Build a deterministic in-memory COG
import numpy as np, rasterio
from rasterio.io import MemoryFile
from rasterio.transform import from_origin
def make_cog(width=512, height=512):
data = np.arange(width * height, dtype="uint16").reshape(height, width)
profile = {
"driver": "GTiff", "dtype": "uint16", "count": 1,
"width": width, "height": height,
"crs": "EPSG:3857", "transform": from_origin(0, height, 1, 1),
"tiled": True, "blockxsize": 256, "blockysize": 256, # COG needs tiling
}
mem = MemoryFile()
with mem.open(**profile) as dst:
dst.write(data, 1)
dst.build_overviews([2, 4], rasterio.enums.Resampling.nearest)
return mem
Step 2 — Read a window instead of the whole raster
from rasterio.windows import Window
def read_window(mem, col_off, row_off, size):
with mem.open() as src:
return src.read(1, window=Window(col_off, row_off, size, size))
Step 3 — Assert only the window was read
The returned array’s shape proves the reader took a sub-region, not the full raster.
def test_windowed_read_is_bounded():
mem = make_cog()
tile = read_window(mem, 0, 0, 64)
assert tile.shape == (64, 64), "reader must return only the requested window"
assert tile[0, 0] == 0 # deterministic pixel value
Step 4 — Mock range requests for a remote reader
When the code under test reads via a URL, patch the HTTP layer so the test asserts range requests without a network — the same isolation principle as mocking PostGIS connections.
from unittest.mock import patch
def test_reader_uses_range_requests():
mem = make_cog()
with patch("myapp.cog.open_remote", return_value=mem):
tile = read_window(mem, 128, 128, 32)
assert tile.shape == (32, 32) # no network touched
Choosing the layer to mock
There are four points in the stack where a COG read can be intercepted, and picking the wrong one produces a test that is either fragile or meaningless. The rule is the same as elsewhere: intercept at the lowest layer that still makes the assertion possible, because everything below the interception point is no longer under test.
Mock nothing — build a real tiled file in a MemoryFile. The best default. The GDAL driver, the tiling, the windowed read logic and the profile round trip all execute for real; only the network is absent. Almost every COG test that is really about reading belongs here.
Mock the HTTP range request. The right layer when the assertion is about which bytes were requested — that a window read fetched two tiles rather than the whole file, that the header was fetched once and cached, that a retry happened on a truncated response. This is the only layer that can observe those, and it is worth the extra setup for exactly those assertions.
Mock the reader object. Appropriate only when the raster is incidental — a function that takes an open dataset and does something unrelated to pixels. It proves nothing about the format and should never be used to stand in for a real read.
Mock the whole storage client. Reserved for error paths that are otherwise impossible to provoke: a credential expiry mid-read, a bucket that starts returning 503, a connection reset after the header. These are legitimate and rare.
| Assertion you want to make | Intercept at | Why not lower |
|---|---|---|
| The window returns the right pixels | Nothing — real MemoryFile | Nothing below matters |
| Only the overlapping tiles were fetched | HTTP range layer | The driver hides the request pattern |
| A truncated response is retried | HTTP range layer | Cannot be provoked from a real file |
| Credentials expiring mid-read is handled | Storage client | No lower layer can express it |
| A function that takes a dataset works | The reader object | The raster is not the subject |
The middle row of the table is the one worth building properly, because it is the assertion nobody else can make. A test that proves a window read fetched two tiles rather than the whole object is the only defence against a subtle regression — a changed block size, a lost tiling option, an accidental full read in a helper — that costs nothing in correctness and a great deal in cloud egress. It fails silently in every other kind of test, and it is exactly the failure a COG pipeline exists to avoid.
Verification pattern
Confirm the fixture is a real COG with overviews, so the test exercises the overview path rather than a plain tiled TIFF.
def test_fixture_has_overviews():
with make_cog().open() as src:
assert src.overviews(1) == [2, 4], "fixture must carry overview levels"
Failure modes and edge cases
- Untiled fixture. A GeoTIFF without
tiled=Trueis not a COG and cannot be read by window efficiently; set block sizes. - Missing overviews. Without
build_overviews, a zoomed-out read pulls full resolution; add overview factors so the reader can pick a level. - Asserting on timing. Testing that a read is “fast” is flaky; assert the returned window shape and byte count instead.
- MemoryFile leak. Not closing the
MemoryFileleaks native handles across tests; use it as a context manager or close in teardown. - CRS/transform mismatch. A fixture whose transform does not match its CRS units makes windowed geographic queries land wrong; keep the transform consistent with the declared CRS.
One habit makes all of this durable: assert the mock is actually a COG before relying on it. Reading back the profile and checking that the file is tiled, that the block size matches what production writes, and that overviews are present takes three lines and prevents the whole category of test that looks like a COG test and is really a GeoTIFF test. It is the same meta-assertion logic that keeps an edge-case generator honest, applied to a format instead of a defect.
Conclusion
Mocking COG reads with rasterio.MemoryFile makes raster tests local, deterministic and — crucially — able to assert the reader uses windows and overviews rather than pulling the whole object. Build a tiled, overview-bearing in-memory fixture, read by window, and patch the HTTP layer for remote readers, and the suite proves the exact partial-read behaviour a COG exists to provide. For the broader raster context, return to raster mocking techniques.