diff --git a/test/camera/meson.build b/test/camera/meson.build
index 6da297714f34a4e3..8f1cfe5a7a5c2884 100644
--- a/test/camera/meson.build
+++ b/test/camera/meson.build
@@ -3,6 +3,7 @@
 camera_tests = [
   [ 'format_default',        'format_default.cpp' ],
   [ 'format_set',            'format_set.cpp' ],
+  [ 'statemachine',          'statemachine.cpp' ],
   [ 'capture',               'capture.cpp' ],
 ]
 
diff --git a/test/camera/statemachine.cpp b/test/camera/statemachine.cpp
new file mode 100644
index 0000000000000000..f4395f2b1bfed698
--- /dev/null
+++ b/test/camera/statemachine.cpp
@@ -0,0 +1,275 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * libcamera Camera API tests
+ */
+
+#include <iostream>
+
+#include "camera_test.h"
+
+using namespace std;
+
+namespace {
+
+class Statemachine : public CameraTest
+{
+protected:
+	int testAvailable()
+	{
+		/* Test operations which should fail. */
+		if (camera_->configureStreams(defconf_) != -EACCES)
+			return TestFail;
+
+		if (camera_->allocateBuffers() != -EACCES)
+			return TestFail;
+
+		if (camera_->freeBuffers() != -EACCES)
+			return TestFail;
+
+		if (camera_->createRequest())
+			return TestFail;
+
+		if (camera_->start() != -EACCES)
+			return TestFail;
+
+		Request request(camera_.get());
+		if (camera_->queueRequest(&request) != -EACCES)
+			return TestFail;
+
+		if (camera_->stop() != -EACCES)
+			return TestFail;
+
+		/* Test operations which should pass. */
+		if (camera_->release())
+			return TestFail;
+
+		/* Test valid state transitions, end in Acquired state. */
+		if (camera_->acquire())
+			return TestFail;
+
+		return TestPass;
+	}
+
+	int testAcquired()
+	{
+		/* Test operations which should fail. */
+		if (camera_->acquire() != -EBUSY)
+			return TestFail;
+
+		if (camera_->allocateBuffers() != -EACCES)
+			return TestFail;
+
+		if (camera_->freeBuffers() != -EACCES)
+			return TestFail;
+
+		if (camera_->createRequest())
+			return TestFail;
+
+		if (camera_->start() != -EACCES)
+			return TestFail;
+
+		Request request(camera_.get());
+		if (camera_->queueRequest(&request) != -EACCES)
+			return TestFail;
+
+		if (camera_->stop() != -EACCES)
+			return TestFail;
+
+		/* Test valid state transitions, end in Configured state. */
+		if (camera_->release())
+			return TestFail;
+
+		if (camera_->acquire())
+			return TestFail;
+
+		if (camera_->configureStreams(defconf_))
+			return TestFail;
+
+		return TestPass;
+	}
+
+	int testConfigured()
+	{
+		/* Test operations which should fail. */
+		if (camera_->acquire() != -EBUSY)
+			return TestFail;
+
+		if (camera_->freeBuffers() != -EACCES)
+			return TestFail;
+
+		if (camera_->createRequest())
+			return TestFail;
+
+		Request request(camera_.get());
+		if (camera_->queueRequest(&request) != -EACCES)
+			return TestFail;
+
+		if (camera_->start() != -EACCES)
+			return TestFail;
+
+		if (camera_->stop() != -EACCES)
+			return TestFail;
+
+		/* Test operations which should pass. */
+		if (camera_->configureStreams(defconf_))
+			return TestFail;
+
+		/* Test valid state transitions, end in Prepared state. */
+		if (camera_->release())
+			return TestFail;
+
+		if (camera_->acquire())
+			return TestFail;
+
+		if (camera_->configureStreams(defconf_))
+			return TestFail;
+
+		if (camera_->allocateBuffers())
+			return TestFail;
+
+		return TestPass;
+	}
+
+	int testPrepared()
+	{
+		/* Test operations which should fail. */
+		if (camera_->acquire() != -EBUSY)
+			return TestFail;
+
+		if (camera_->release() != -EBUSY)
+			return TestFail;
+
+		if (camera_->configureStreams(defconf_) != -EACCES)
+			return TestFail;
+
+		if (camera_->allocateBuffers() != -EACCES)
+			return TestFail;
+
+		Request request1(camera_.get());
+		if (camera_->queueRequest(&request1) != -EACCES)
+			return TestFail;
+
+		if (camera_->stop() != -EACCES)
+			return TestFail;
+
+		/* Test operations which should pass. */
+		Request *request2 = camera_->createRequest();
+		if (!request2)
+			return TestFail;
+
+		/* Never handed to hardware so need to manually delete it. */
+		delete request2;
+
+		/* Test valid state transitions, end in Running state. */
+		if (camera_->freeBuffers())
+			return TestFail;
+
+		if (camera_->release())
+			return TestFail;
+
+		if (camera_->acquire())
+			return TestFail;
+
+		if (camera_->configureStreams(defconf_))
+			return TestFail;
+
+		if (camera_->allocateBuffers())
+			return TestFail;
+
+		if (camera_->start())
+			return TestFail;
+
+		return TestPass;
+	}
+
+	int testRuning()
+	{
+		/* Test operations which should fail. */
+		if (camera_->acquire() != -EBUSY)
+			return TestFail;
+
+		if (camera_->release() != -EBUSY)
+			return TestFail;
+
+		if (camera_->configureStreams(defconf_) != -EACCES)
+			return TestFail;
+
+		if (camera_->allocateBuffers() != -EACCES)
+			return TestFail;
+
+		if (camera_->freeBuffers() != -EACCES)
+			return TestFail;
+
+		if (camera_->start() != -EACCES)
+			return TestFail;
+
+		/* Test operations which should pass. */
+		Request *request = camera_->createRequest();
+		if (!request)
+			return TestFail;
+
+		Stream *stream = *camera_->streams().begin();
+		BufferPool &pool = stream->bufferPool();
+		Buffer &buffer = pool.buffers().front();
+		std::map<Stream *, Buffer *> map = { { stream, &buffer } };
+		if (request->setBuffers(map))
+			return TestFail;
+
+		if (camera_->queueRequest(request))
+			return TestFail;
+
+		/* Test valid state transitions, end in Available state. */
+		if (camera_->stop())
+			return TestFail;
+
+		if (camera_->freeBuffers())
+			return TestFail;
+
+		if (camera_->release())
+			return TestFail;
+
+		return TestPass;
+	}
+
+	int run()
+	{
+		Stream *stream = *camera_->streams().begin();
+		std::set<Stream *> streams = { stream };
+		defconf_ = camera_->streamConfiguration(streams);
+
+		if (testAvailable() != TestPass) {
+			cout << "State machine in Available state failed" << endl;
+			return TestFail;
+		}
+
+		if (testAcquired() != TestPass) {
+			cout << "State machine in Acquired state failed" << endl;
+			return TestFail;
+		}
+
+		if (testConfigured() != TestPass) {
+			cout << "State machine in Configured state failed" << endl;
+			return TestFail;
+		}
+
+		if (testPrepared() != TestPass) {
+			cout << "State machine in Prepared state failed" << endl;
+			return TestFail;
+		}
+
+		if (testRuning() != TestPass) {
+			cout << "State machine in Running state failed" << endl;
+			return TestFail;
+		}
+
+		return TestPass;
+	}
+
+	std::map<Stream *, StreamConfiguration> defconf_;
+};
+
+} /* namespace */
+
+TEST_REGISTER(Statemachine);
