diff --git a/test/net/sf/briar/transport/ConnectionReaderImplTest.java b/test/net/sf/briar/transport/ConnectionReaderImplTest.java
index e975ce539eb7744c0d51d6ca8ed0db2d102847b8..f00b611ba90d7263ce420b6a55c81c963a68aaca 100644
--- a/test/net/sf/briar/transport/ConnectionReaderImplTest.java
+++ b/test/net/sf/briar/transport/ConnectionReaderImplTest.java
@@ -1,12 +1,103 @@
 package net.sf.briar.transport;
 
-import org.junit.Test;
-
+import static net.sf.briar.api.transport.TransportConstants.HEADER_LENGTH;
+import static net.sf.briar.api.transport.TransportConstants.MAC_LENGTH;
 import net.sf.briar.BriarTestCase;
 
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.junit.Test;
+
 public class ConnectionReaderImplTest extends BriarTestCase {
 
-	// FIXME: Write tests
+	private static final int FRAME_LENGTH = 1024;
+	private static final int MAX_PAYLOAD_LENGTH =
+			FRAME_LENGTH - HEADER_LENGTH - MAC_LENGTH;
+
+	@Test
+	public void testEmptyFramesAreSkipped() throws Exception {
+		Mockery context = new Mockery();
+		final FrameReader reader = context.mock(FrameReader.class);
+		context.checking(new Expectations() {{
+			oneOf(reader).readFrame(with(any(byte[].class)));
+			will(returnValue(0)); // Empty frame
+			oneOf(reader).readFrame(with(any(byte[].class)));
+			will(returnValue(2)); // Non-empty frame with two payload bytes
+			oneOf(reader).readFrame(with(any(byte[].class)));
+			will(returnValue(0)); // Empty frame
+			oneOf(reader).readFrame(with(any(byte[].class)));
+			will(returnValue(-1)); // No more frames
+		}});
+		ConnectionReaderImpl c = new ConnectionReaderImpl(reader, FRAME_LENGTH);
+		assertEquals(0, c.read()); // Skip the first empty frame, read a byte
+		assertEquals(0, c.read()); // Read another byte
+		assertEquals(-1, c.read()); // Skip the second empty frame, reach EOF
+		assertEquals(-1, c.read()); // Still at EOF
+	}
+
+	@Test
+	public void testEmptyFramesAreSkippedWithBuffer() throws Exception {
+		Mockery context = new Mockery();
+		final FrameReader reader = context.mock(FrameReader.class);
+		context.checking(new Expectations() {{
+			oneOf(reader).readFrame(with(any(byte[].class)));
+			will(returnValue(0)); // Empty frame
+			oneOf(reader).readFrame(with(any(byte[].class)));
+			will(returnValue(2)); // Non-empty frame with two payload bytes
+			oneOf(reader).readFrame(with(any(byte[].class)));
+			will(returnValue(0)); // Empty frame
+			oneOf(reader).readFrame(with(any(byte[].class)));
+			will(returnValue(-1)); // No more frames
+		}});
+		ConnectionReaderImpl c = new ConnectionReaderImpl(reader, FRAME_LENGTH);
+		byte[] buf = new byte[MAX_PAYLOAD_LENGTH];
+		// Skip the first empty frame, read the two payload bytes
+		assertEquals(2, c.read(buf));
+		// Skip the second empty frame, reach EOF
+		assertEquals(-1, c.read(buf));
+		// Still at EOF
+		assertEquals(-1, c.read(buf));
+	}
+
+	@Test
+	public void testMultipleReadsPerFrame() throws Exception {
+		Mockery context = new Mockery();
+		final FrameReader reader = context.mock(FrameReader.class);
+		context.checking(new Expectations() {{
+			oneOf(reader).readFrame(with(any(byte[].class)));
+			will(returnValue(MAX_PAYLOAD_LENGTH)); // Nice long frame
+			oneOf(reader).readFrame(with(any(byte[].class)));
+			will(returnValue(-1)); // No more frames
+		}});
+		ConnectionReaderImpl c = new ConnectionReaderImpl(reader, FRAME_LENGTH);
+		byte[] buf = new byte[MAX_PAYLOAD_LENGTH / 2];
+		// Read the first half of the payload
+		assertEquals(MAX_PAYLOAD_LENGTH / 2, c.read(buf));
+		// Read the second half of the payload
+		assertEquals(MAX_PAYLOAD_LENGTH / 2, c.read(buf));
+		// Reach EOF
+		assertEquals(-1, c.read(buf, 0, buf.length));
+	}
+
 	@Test
-	public void testNothing() {}
+	public void testMultipleReadsPerFrameWithOffsets() throws Exception {
+		Mockery context = new Mockery();
+		final FrameReader reader = context.mock(FrameReader.class);
+		context.checking(new Expectations() {{
+			oneOf(reader).readFrame(with(any(byte[].class)));
+			will(returnValue(MAX_PAYLOAD_LENGTH)); // Nice long frame
+			oneOf(reader).readFrame(with(any(byte[].class)));
+			will(returnValue(-1)); // No more frames
+		}});
+		ConnectionReaderImpl c = new ConnectionReaderImpl(reader, FRAME_LENGTH);
+		byte[] buf = new byte[MAX_PAYLOAD_LENGTH];
+		// Read the first half of the payload
+		assertEquals(MAX_PAYLOAD_LENGTH / 2, c.read(buf, MAX_PAYLOAD_LENGTH / 2,
+				MAX_PAYLOAD_LENGTH / 2));
+		// Read the second half of the payload
+		assertEquals(MAX_PAYLOAD_LENGTH / 2, c.read(buf, 123,
+				MAX_PAYLOAD_LENGTH / 2));
+		// Reach EOF
+		assertEquals(-1, c.read(buf, 0, buf.length));
+	}
 }
diff --git a/test/net/sf/briar/transport/NullIncomingEncryptionLayer.java b/test/net/sf/briar/transport/NullIncomingEncryptionLayer.java
deleted file mode 100644
index 96e4729fe17c17dbdbeed4ccb9161cae9de0cceb..0000000000000000000000000000000000000000
--- a/test/net/sf/briar/transport/NullIncomingEncryptionLayer.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package net.sf.briar.transport;
-
-import static net.sf.briar.api.transport.TransportConstants.HEADER_LENGTH;
-import static net.sf.briar.api.transport.TransportConstants.MAC_LENGTH;
-import static net.sf.briar.api.transport.TransportConstants.MAX_FRAME_LENGTH;
-
-import java.io.EOFException;
-import java.io.IOException;
-import java.io.InputStream;
-
-import net.sf.briar.api.FormatException;
-
-/** An encryption layer that performs no encryption. */
-class NullIncomingEncryptionLayer implements FrameReader {
-
-	private final InputStream in;
-
-	NullIncomingEncryptionLayer(InputStream in) {
-		this.in = in;
-	}
-
-	public int readFrame(byte[] frame) throws IOException {
-		// Read the frame
-		int ciphertextLength = 0;
-		while(ciphertextLength < MAX_FRAME_LENGTH) {
-			int read = in.read(frame, ciphertextLength,
-					MAX_FRAME_LENGTH - ciphertextLength);
-			if(read == -1) break; // We'll check the length later
-			ciphertextLength += read;
-		}
-		int plaintextLength = ciphertextLength - MAC_LENGTH;
-		if(plaintextLength < HEADER_LENGTH) throw new EOFException();
-		// Decode and validate the header
-		boolean lastFrame = FrameEncoder.isLastFrame(frame);
-		if(!lastFrame && ciphertextLength < MAX_FRAME_LENGTH)
-			throw new EOFException();
-		int payloadLength = FrameEncoder.getPayloadLength(frame);
-		if(payloadLength > plaintextLength - HEADER_LENGTH)
-			throw new FormatException();
-		// If there's any padding it must be all zeroes
-		for(int i = HEADER_LENGTH + payloadLength; i < plaintextLength; i++)
-			if(frame[i] != 0) throw new FormatException();
-		return payloadLength;
-	}
-}
diff --git a/test/net/sf/briar/transport/NullOutgoingEncryptionLayer.java b/test/net/sf/briar/transport/NullOutgoingEncryptionLayer.java
deleted file mode 100644
index a9dd1332a68079cef03235ef85bf9ea97b076da6..0000000000000000000000000000000000000000
--- a/test/net/sf/briar/transport/NullOutgoingEncryptionLayer.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package net.sf.briar.transport;
-
-import static net.sf.briar.api.transport.TransportConstants.HEADER_LENGTH;
-import static net.sf.briar.api.transport.TransportConstants.MAC_LENGTH;
-
-import java.io.IOException;
-import java.io.OutputStream;
-
-/** An encryption layer that performs no encryption. */
-class NullOutgoingEncryptionLayer implements FrameWriter {
-
-	private final OutputStream out;
-
-	private long capacity;
-
-	NullOutgoingEncryptionLayer(OutputStream out) {
-		this.out = out;
-		capacity = Long.MAX_VALUE;
-	}
-
-	NullOutgoingEncryptionLayer(OutputStream out, long capacity) {
-		this.out = out;
-		this.capacity = capacity;
-	}
-
-	public void writeFrame(byte[] frame, int payloadLength, int paddingLength,
-			boolean lastFrame) throws IOException {
-		int plaintextLength = HEADER_LENGTH + payloadLength + paddingLength;
-		int ciphertextLength = plaintextLength + MAC_LENGTH;
-		// Encode the header
-		FrameEncoder.encodeHeader(frame, lastFrame, payloadLength);
-		// If there's any padding it must all be zeroes
-		for(int i = HEADER_LENGTH + payloadLength; i < plaintextLength; i++)
-			frame[i] = 0;
-		// Write the frame
-		out.write(frame, 0, ciphertextLength);
-		capacity -= ciphertextLength;
-	}
-
-	public void flush() throws IOException {
-		out.flush();
-	}
-
-	public long getRemainingCapacity() {
-		return capacity;
-	}
-}