diff --git a/test/camera/buffer_import.cpp b/test/camera/buffer_import.cpp
index 3ba6ce9690f29329..327db7912c09ed2d 100644
--- a/test/camera/buffer_import.cpp
+++ b/test/camera/buffer_import.cpp
@@ -22,19 +22,32 @@
 
 using namespace libcamera;
 
-/* Keep SINK_BUFFER_COUNT > CAMERA_BUFFER_COUNT + 1 */
-static constexpr unsigned int SINK_BUFFER_COUNT = 8;
-static constexpr unsigned int CAMERA_BUFFER_COUNT = 4;
+namespace {
 
-class FrameSink
+/* A provider of external buffers, suitable for import by a Camera. */
+class BufferSource
 {
 public:
-	FrameSink()
+	BufferSource()
 		: video_(nullptr)
 	{
 	}
 
-	int init()
+	~BufferSource()
+	{
+		if (video_) {
+			video_->releaseBuffers();
+			video_->close();
+		}
+
+		delete video_;
+		video_ = nullptr;
+
+		if (media_)
+			media_->release();
+	}
+
+	int allocate(const StreamConfiguration &config)
 	{
 		int ret;
 
@@ -72,187 +85,30 @@ public:
 			return TestFail;
 
 		/* Configure the format. */
-		ret = video_->getFormat(&format_);
+		V4L2DeviceFormat format;
+		ret = video_->getFormat(&format);
 		if (ret) {
 			std::cout << "Failed to get format on output device" << std::endl;
 			return ret;
 		}
 
-		format_.size.width = 1920;
-		format_.size.height = 1080;
-		format_.fourcc = V4L2_PIX_FMT_RGB24;
-		format_.planesCount = 1;
-		format_.planes[0].size = 1920 * 1080 * 3;
-		format_.planes[0].bpl = 1920 * 3;
-
-		if (video_->setFormat(&format_)) {
-			cleanup();
-			return TestFail;
-		}
-
-		/* Export the buffers to a pool. */
-		pool_.createBuffers(SINK_BUFFER_COUNT);
-		ret = video_->exportBuffers(&pool_);
-		if (ret) {
-			std::cout << "Failed to export buffers" << std::endl;
-			cleanup();
+		format.size = config.size;
+		format.fourcc = V4L2VideoDevice::toV4L2Fourcc(config.pixelFormat, false);
+		if (video_->setFormat(&format))
 			return TestFail;
-		}
-
-		/* Only use the first CAMERA_BUFFER_COUNT buffers to start with. */
-		availableBuffers_.resize(CAMERA_BUFFER_COUNT);
-		std::iota(availableBuffers_.begin(), availableBuffers_.end(), 0);
-
-		/* Connect the buffer ready signal. */
-		video_->bufferReady.connect(this, &FrameSink::bufferComplete);
-
-		return TestPass;
-	}
-
-	void cleanup()
-	{
-		if (video_) {
-			video_->streamOff();
-			video_->releaseBuffers();
-			video_->close();
-
-			delete video_;
-			video_ = nullptr;
-		}
-
-		if (media_)
-			media_->release();
-	}
-
-	int start()
-	{
-		requestsCount_ = 0;
-		done_ = false;
-
-		int ret = video_->streamOn();
-		if (ret < 0)
-			return ret;
-
-		/* Queue all the initial requests. */
-		for (unsigned int index = 0; index < CAMERA_BUFFER_COUNT; ++index)
-			queueRequest(index);
 
-		return 0;
+		return video_->exportBuffers(config.bufferCount, &buffers_);
 	}
 
-	int stop()
+	const std::vector<std::unique_ptr<FrameBuffer>> &buffers()
 	{
-		return video_->streamOff();
+		return buffers_;
 	}
 
-	void requestComplete(uint64_t cookie, const Buffer *metadata)
-	{
-		unsigned int index = cookie;
-
-		Buffer *buffer = new Buffer(index, metadata);
-		int ret = video_->queueBuffer(buffer);
-		if (ret < 0)
-			std::cout << "Failed to queue buffer to sink" << std::endl;
-	}
-
-	bool done() const { return done_; }
-
-	PixelFormat format() const
-	{
-		return video_->toPixelFormat(format_.fourcc);
-	}
-
-	const Size &size() const
-	{
-		return format_.size;
-	}
-
-	Signal<uint64_t, int> requestReady;
-
 private:
-	void queueRequest(unsigned int index)
-	{
-		auto it = std::find(availableBuffers_.begin(),
-				    availableBuffers_.end(), index);
-		availableBuffers_.erase(it);
-
-		uint64_t cookie = index;
-		BufferMemory &mem = pool_.buffers()[index];
-		int dmabuf = mem.planes()[0].fd.fd();
-
-		requestReady.emit(cookie, dmabuf);
-
-		requestsCount_++;
-	}
-
-	void bufferComplete(Buffer *buffer)
-	{
-		availableBuffers_.push_back(buffer->index());
-
-		/*
-		 * Pick the buffer for the next request among the available
-		 * buffers.
-		 *
-		 * For the first 20 frames, select the buffer that has just
-		 * completed to keep the mapping of dmabuf fds to buffers
-		 * unchanged in the camera.
-		 *
-		 * For the next 20 frames, cycle randomly over the available
-		 * buffers. The mapping should still be kept unchanged, as the
-		 * camera should map using the cached fds.
-		 *
-		 * For the last 20 frames, cycles through all buffers, which
-		 * should trash the mappings.
-		 */
-		unsigned int index = buffer->index();
-		delete buffer;
-
-		std::cout << "Completed buffer, request=" << requestsCount_
-			  << ", available buffers=" << availableBuffers_.size()
-			  << std::endl;
-
-		if (requestsCount_ >= 60) {
-			if (availableBuffers_.size() == SINK_BUFFER_COUNT)
-				done_ = true;
-			return;
-		}
-
-		if (requestsCount_ == 40) {
-			/* Add the remaining of the buffers. */
-			for (unsigned int i = CAMERA_BUFFER_COUNT;
-			     i < SINK_BUFFER_COUNT; ++i)
-				availableBuffers_.push_back(i);
-		}
-
-		if (requestsCount_ >= 20) {
-			/*
-			 * Wait until we have enough buffers to make this
-			 * meaningful. Preferably half of the camera buffers,
-			 * but no less than 2 in any case.
-			 */
-			const unsigned int min_pool_size =
-				std::min(CAMERA_BUFFER_COUNT / 2, 2U);
-			if (availableBuffers_.size() < min_pool_size)
-				return;
-
-			/* Pick a buffer at random. */
-			unsigned int pos = random_() % availableBuffers_.size();
-			index = availableBuffers_[pos];
-		}
-
-		queueRequest(index);
-	}
-
 	std::shared_ptr<MediaDevice> media_;
 	V4L2VideoDevice *video_;
-	BufferPool pool_;
-	V4L2DeviceFormat format_;
-
-	unsigned int requestsCount_;
-	std::vector<int> availableBuffers_;
-	std::random_device random_;
-
-	bool done_;
+	std::vector<std::unique_ptr<FrameBuffer>> buffers_;
 };
 
 class BufferImportTest : public CameraTest, public Test
@@ -263,175 +119,152 @@ public:
 	{
 	}
 
-	void queueRequest(uint64_t cookie, int dmabuf)
-	{
-		Request *request = camera_->createRequest(cookie);
-
-		std::unique_ptr<Buffer> buffer = stream_->createBuffer({ dmabuf, -1, -1 });
-		request->addBuffer(stream_, move(buffer));
-		camera_->queueRequest(request);
-	}
-
 protected:
 	void bufferComplete(Request *request, Buffer *buffer)
 	{
 		if (buffer->metadata().status != FrameMetadata::FrameSuccess)
 			return;
 
-		unsigned int index = buffer->index();
-		int dmabuf = buffer->dmabufs()[0];
-
-		/* Record dmabuf to index remappings. */
-		bool remapped = false;
-		if (bufferMappings_.find(index) != bufferMappings_.end()) {
-			if (bufferMappings_[index] != dmabuf)
-				remapped = true;
-		}
+		completeBuffersCount_++;
+	}
 
-		std::cout << "Completed request " << framesCaptured_
-			  << ": dmabuf fd " << dmabuf
-			  << " -> index " << index
-			  << " (" << (remapped ? 'R' : '-') << ")"
-			  << std::endl;
+	void requestComplete(Request *request)
+	{
+		if (request->status() != Request::RequestComplete)
+			return;
 
-		if (remapped)
-			bufferRemappings_.push_back(framesCaptured_);
+		const std::map<Stream *, Buffer *> &buffers = request->buffers();
 
-		bufferMappings_[index] = dmabuf;
-		framesCaptured_++;
+		completeRequestsCount_++;
 
-		sink_.requestComplete(request->cookie(), buffer);
+		/* Create a new request. */
+		Stream *stream = buffers.begin()->first;
+		int dmabuf = buffers.begin()->second->dmabufs()[0];
+		std::unique_ptr<Buffer> buffer = stream->createBuffer({ dmabuf, -1, -1 });
 
-		if (framesCaptured_ == 60)
-			sink_.stop();
+		request = camera_->createRequest();
+		request->addBuffer(stream, std::move(buffer));
+		camera_->queueRequest(request);
 	}
 
-	int initCamera()
+	int init() override
 	{
-		if (camera_->acquire()) {
-			std::cout << "Failed to acquire the camera" << std::endl;
-			return TestFail;
-		}
+		if (status_ != TestPass)
+			return status_;
 
-		/*
-		 * Configure the Stream to work with externally allocated
-		 * buffers by setting the memoryType to ExternalMemory.
-		 */
-		std::unique_ptr<CameraConfiguration> config;
-		config = camera_->generateConfiguration({ StreamRole::VideoRecording });
-		if (!config || config->size() != 1) {
-			std::cout << "Failed to generate configuration" << std::endl;
+		config_ = camera_->generateConfiguration({ StreamRole::VideoRecording });
+		if (!config_ || config_->size() != 1) {
+			std::cout << "Failed to generate default configuration" << std::endl;
 			return TestFail;
 		}
 
-		StreamConfiguration &cfg = config->at(0);
-		cfg.size = sink_.size();
-		cfg.pixelFormat = sink_.format();
-		cfg.bufferCount = CAMERA_BUFFER_COUNT;
+		StreamConfiguration &cfg = config_->at(0);
 		cfg.memoryType = ExternalMemory;
 
-		if (camera_->configure(config.get())) {
-			std::cout << "Failed to set configuration" << std::endl;
+		return TestPass;
+	}
+
+	int run() override
+	{
+		StreamConfiguration &cfg = config_->at(0);
+
+		if (camera_->acquire()) {
+			std::cout << "Failed to acquire the camera" << std::endl;
 			return TestFail;
 		}
 
-		stream_ = cfg.stream();
+		if (camera_->configure(config_.get())) {
+			std::cout << "Failed to set default configuration" << std::endl;
+			return TestFail;
+		}
 
-		/* Allocate buffers. */
 		if (camera_->allocateBuffers()) {
 			std::cout << "Failed to allocate buffers" << std::endl;
 			return TestFail;
 		}
 
-		/* Connect the buffer completed signal. */
-		camera_->bufferCompleted.connect(this, &BufferImportTest::bufferComplete);
+		Stream *stream = cfg.stream();
 
-		return TestPass;
-	}
+		BufferSource source;
+		int ret = source.allocate(cfg);
+		if (ret < 0)
+			return TestFail;
 
-	int init()
-	{
-		if (status_ != TestPass)
-			return status_;
+		std::vector<Request *> requests;
+		for (const std::unique_ptr<FrameBuffer> &framebuffer : source.buffers()) {
+			int dmabuf = framebuffer->planes()[0].fd.fd();
 
-		int ret = sink_.init();
-		if (ret != TestPass) {
-			cleanup();
-			return ret;
-		}
+			Request *request = camera_->createRequest();
+			if (!request) {
+				std::cout << "Failed to create request" << std::endl;
+				return TestFail;
+			}
 
-		ret = initCamera();
-		if (ret != TestPass) {
-			cleanup();
-			return ret;
-		}
+			std::unique_ptr<Buffer> buffer = stream->createBuffer({ dmabuf, -1, -1 });
+			if (request->addBuffer(stream, std::move(buffer))) {
+				std::cout << "Failed to associating buffer with request" << std::endl;
+				return TestFail;
+			}
 
-		sink_.requestReady.connect(this, &BufferImportTest::queueRequest);
-		return TestPass;
-	}
+			requests.push_back(request);
+		}
 
-	int run()
-	{
-		int ret;
+		completeRequestsCount_ = 0;
+		completeBuffersCount_ = 0;
 
-		framesCaptured_ = 0;
+		camera_->bufferCompleted.connect(this, &BufferImportTest::bufferComplete);
+		camera_->requestCompleted.connect(this, &BufferImportTest::requestComplete);
 
 		if (camera_->start()) {
 			std::cout << "Failed to start camera" << std::endl;
 			return TestFail;
 		}
 
-		ret = sink_.start();
-		if (ret < 0) {
-			std::cout << "Failed to start sink" << std::endl;
-			return TestFail;
+		for (Request *request : requests) {
+			if (camera_->queueRequest(request)) {
+				std::cout << "Failed to queue request" << std::endl;
+				return TestFail;
+			}
 		}
 
 		EventDispatcher *dispatcher = cm_->eventDispatcher();
 
 		Timer timer;
-		timer.start(5000);
-		while (timer.isRunning() && !sink_.done())
+		timer.start(1000);
+		while (timer.isRunning())
 			dispatcher->processEvents();
 
-		std::cout << framesCaptured_ << " frames captured, "
-			  << bufferRemappings_.size() << " buffers remapped"
-			  << std::endl;
+		if (completeRequestsCount_ <= cfg.bufferCount * 2) {
+			std::cout << "Failed to capture enough frames (got "
+				  << completeRequestsCount_ << " expected at least "
+				  << cfg.bufferCount * 2 << ")" << std::endl;
+			return TestFail;
+		}
 
-		if (framesCaptured_ < 60) {
-			std::cout << "Too few frames captured" << std::endl;
+		if (completeRequestsCount_ != completeBuffersCount_) {
+			std::cout << "Number of completed buffers and requests differ" << std::endl;
 			return TestFail;
 		}
 
-		if (bufferRemappings_.empty()) {
-			std::cout << "No buffer remappings" << std::endl;
+		if (camera_->stop()) {
+			std::cout << "Failed to stop camera" << std::endl;
 			return TestFail;
 		}
 
-		if (bufferRemappings_[0] < 40) {
-			std::cout << "Early buffer remapping" << std::endl;
+		if (camera_->freeBuffers()) {
+			std::cout << "Failed to free buffers" << std::endl;
 			return TestFail;
 		}
 
 		return TestPass;
 	}
 
-	void cleanup()
-	{
-		sink_.cleanup();
-
-		camera_->stop();
-		camera_->freeBuffers();
-	}
-
 private:
-	Stream *stream_;
-
-	std::map<unsigned int, int> bufferMappings_;
-	std::vector<unsigned int> bufferRemappings_;
-	unsigned int framesCaptured_;
-
-	FrameSink sink_;
+	unsigned int completeBuffersCount_;
+	unsigned int completeRequestsCount_;
+	std::unique_ptr<CameraConfiguration> config_;
 };
 
+} /* namespace */
+
 TEST_REGISTER(BufferImportTest);
