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.
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
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.
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.