Most OCPP bugs are not electrical. They are a missing field, a wrong enum, a state machine that accepts a StartTransaction it should have rejected. None of that requires current to flow, which means most OCPP testing can happen on a laptop.
This is the setup we recommend to teams who are waiting on hardware, or who have hardware but cannot tie it up for every commit.
Decide which side you are testing
This sounds obvious and is the single most common source of wasted effort. There are two independent things people call "OCPP testing":
Testing a charge point. You have firmware that is supposed to speak OCPP, and you want to know whether it does. You need something that behaves like a CSMS and judges what the charger sends.
Testing a CSMS. You have a backend, and you want to know whether it handles what real chargers do. You need something that behaves like a charger and can be told to misbehave.
They need opposite tools, and a tool built for one is close to useless for the other. Decide first.
Testing a charge point
Point the charger at a CSMS you control and watch what it says. The charger does not know or care whether the endpoint is production infrastructure or a process on your machine.
Set the charger's Central System URL to your endpoint. Almost every charger exposes this in a web UI or a config file, usually as something like:
wss://10.0.1.50:9000/ocpp/CP001Then work through the message flow in order, because later messages depend on earlier ones:
- BootNotification. Check
chargePointVendorandchargePointModelare present — both are required, and both are commonly left empty in early firmware. Confirm the charger honours theintervalyou return rather than using a hardcoded one. - Heartbeat. Confirm it arrives at the interval from your BootNotification response, and that the charger adjusts if you change it via ChangeConfiguration.
- StatusNotification. Walk the connector through Available, Preparing, Charging, Finishing. Watch for transitions the spec does not allow, and for a charger that reports
connectorId: 0when it means the whole station. - StartTransaction / StopTransaction. Check
idTaghandling against every AuthorizationStatus you can return — Accepted, Blocked, Expired, Invalid, ConcurrentTx. Rejecting a transaction is the path that gets tested least and breaks most. - MeterValues. Confirm the sampled interval matches configuration, and that units and
measurandvalues are what you expect.
The most valuable part of this exercise is deliberately returning failures. Firmware that handles Accepted correctly and falls over on Blocked is extremely common, because the happy path is the only one anyone exercised.
Testing a CSMS
Here you need a simulated charger, and the thing that makes a simulator useful is not that it can complete a charging session. It is that it can do things a real charger does when something has gone wrong.
The scenarios worth automating:
- Cold start. BootNotification from a charger the CSMS has never seen. Does it auto-provision, or reject?
- Reconnect mid-transaction. Drop the socket during an active transaction and reconnect. The transaction must survive; the meter values must not restart.
- Clock skew. Send timestamps that are hours off. A CSMS that trusts charger clocks unconditionally will produce billing records that cannot be reconciled.
- Duplicate StartTransaction. Same
idTag, same connector, twice. ConcurrentTx exists for a reason. - Offline queue flush. Buffer messages while disconnected, then deliver them in a burst on reconnect. This is where naive CSMS implementations lose data.
- Malformed payloads. Missing required fields, wrong types, unknown enum values. The CSMS should return a CALLERROR, not close the connection or crash the worker.
Scale matters too, but later than people think. One hundred simulated chargers will find your concurrency bugs; ten thousand mostly finds your database's limits, which is a different investigation.
Put it in CI
The reason to do all this in software is that software can run on every commit. A useful OCPP CI job looks like:
- Start the CSMS under test
- Start N simulated chargers against it
- Run a scripted scenario — connect, boot, authorize, charge, stop, disconnect
- Assert on the resulting state: transactions closed, meter values monotonic, no orphaned sessions
- Tear down and report
This catches the regressions that hardware testing catches too late. A change that breaks StopTransaction reason codes is cheap to find in CI and expensive to find in a field trial.
What this does not cover
Being honest about the limits matters, because a green test suite that hides a class of failure is worse than no suite.
Electrical and safety behaviour. Contactor timing, ground fault detection, temperature derating, emergency stop. None of it is visible over OCPP, and none of it can be simulated.
Real network conditions. Chargers live on cellular links behind carrier NAT. Packet loss, multi-second latency, silent connection drops — a simulator on localhost will not reproduce these, and they are responsible for a large share of production incidents.
Vendor quirks. Every manufacturer interprets some corner of the spec differently. You find these by connecting real hardware from that vendor, and there is no substitute.
Formal certification. Passing your own tests is not OCPP certification. That still runs through the OCA's process and their test tool.
So the shape of a sensible programme is: software testing catches the majority of defects continuously and cheaply, hardware testing confirms the things only hardware can confirm, and certification happens once the first two are clean.
Tooling
There is good open-source work in this space — docile-charge-point is scriptable and has been around for years, and the Python ocpp library gives you building blocks for either side.
We build TestPilot for the first job — a CSMS that judges a charger and exports a conformance report — and SimPilot for the second, simulating chargers against your backend including the failure modes above. If you want the protocol layer inside your own Go service rather than a tool alongside it, OcppPilot is the library the other two are built on.