diff --git a/test/net/sf/briar/transport/ConnectionReaderImplTest.java b/test/net/sf/briar/transport/ConnectionReaderImplTest.java
index d701ed999ea451fc4362b90652f54153f53b54e4..7725d6bf12bdc957020f2a3d91ca3ed24b312400 100644
--- a/test/net/sf/briar/transport/ConnectionReaderImplTest.java
+++ b/test/net/sf/briar/transport/ConnectionReaderImplTest.java
@@ -3,39 +3,19 @@ package net.sf.briar.transport;
 import static net.sf.briar.api.transport.TransportConstants.MAX_FRAME_LENGTH;
 
 import java.io.ByteArrayInputStream;
-import java.io.EOFException;
-import java.io.IOException;
-import java.io.InputStream;
 import java.util.Arrays;
 
-import javax.crypto.Mac;
-
-import junit.framework.TestCase;
 import net.sf.briar.TestUtils;
 import net.sf.briar.api.FormatException;
-import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.transport.ConnectionReader;
-import net.sf.briar.crypto.CryptoModule;
-import net.sf.briar.util.ByteUtils;
 
 import org.apache.commons.io.output.ByteArrayOutputStream;
 import org.junit.Test;
 
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-
-public class ConnectionReaderImplTest extends TestCase {
-
-	private final Mac mac;
-	private final int headerLength = 8, macLength;
+public class ConnectionReaderImplTest extends TransportTest {
 
 	public ConnectionReaderImplTest() throws Exception {
 		super();
-		Injector i = Guice.createInjector(new CryptoModule());
-		CryptoComponent crypto = i.getInstance(CryptoComponent.class);
-		mac = crypto.getMac();
-		mac.init(crypto.generateSecretKey());
-		macLength = mac.getMacLength();
 	}
 
 	@Test
@@ -75,7 +55,6 @@ public class ConnectionReaderImplTest extends TestCase {
 
 	@Test
 	public void testMaxLength() throws Exception {
-		int maxPayloadLength = MAX_FRAME_LENGTH - headerLength - macLength;
 		// First frame: max payload length
 		byte[] frame = new byte[MAX_FRAME_LENGTH];
 		writeHeader(frame, 0L, maxPayloadLength, 0);
@@ -106,7 +85,6 @@ public class ConnectionReaderImplTest extends TestCase {
 
 	@Test
 	public void testMaxLengthWithPadding() throws Exception {
-		int maxPayloadLength = MAX_FRAME_LENGTH - headerLength - macLength;
 		int paddingLength = 10;
 		// First frame: max payload length, including padding
 		byte[] frame = new byte[MAX_FRAME_LENGTH];
@@ -204,35 +182,4 @@ public class ConnectionReaderImplTest extends TestCase {
 			fail();
 		} catch(FormatException expected) {}
 	}
-
-	private void writeHeader(byte[] b, long frame, int payload, int padding) {
-		ByteUtils.writeUint32(frame, b, 0);
-		ByteUtils.writeUint16(payload, b, 4);
-		ByteUtils.writeUint16(padding, b, 6);
-	}
-
-	/** A ConnectionDecrypter that performs no decryption. */
-	private static class NullConnectionDecrypter
-	implements ConnectionDecrypter {
-
-		private final InputStream in;
-
-		private NullConnectionDecrypter(InputStream in) {
-			this.in = in;
-		}
-
-		public InputStream getInputStream() {
-			return in;
-		}
-
-		public void readMac(byte[] mac) throws IOException {
-			int offset = 0;
-			while(offset < mac.length) {
-				int read = in.read(mac, offset, mac.length - offset);
-				if(read == -1) break;
-				offset += read;
-			}
-			if(offset < mac.length) throw new EOFException();
-		}
-	}
 }
diff --git a/test/net/sf/briar/transport/ConnectionWriterImplTest.java b/test/net/sf/briar/transport/ConnectionWriterImplTest.java
index 50dbd389dcb83f211a0b4c0464f1a4516ce2a8ef..4640ab10f698e8298a19742720e93fe11233d2cf 100644
--- a/test/net/sf/briar/transport/ConnectionWriterImplTest.java
+++ b/test/net/sf/briar/transport/ConnectionWriterImplTest.java
@@ -3,35 +3,17 @@ package net.sf.briar.transport;
 import static net.sf.briar.api.transport.TransportConstants.MAX_FRAME_LENGTH;
 
 import java.io.ByteArrayOutputStream;
-import java.io.IOException;
 import java.io.OutputStream;
 import java.util.Arrays;
 
-import javax.crypto.Mac;
-
-import junit.framework.TestCase;
-import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.transport.ConnectionWriter;
-import net.sf.briar.crypto.CryptoModule;
-import net.sf.briar.util.ByteUtils;
 
 import org.junit.Test;
 
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-
-public class ConnectionWriterImplTest extends TestCase {
-
-	private final Mac mac;
-	private final int headerLength = 8, macLength;
+public class ConnectionWriterImplTest extends TransportTest {
 
 	public ConnectionWriterImplTest() throws Exception {
 		super();
-		Injector i = Guice.createInjector(new CryptoModule());
-		CryptoComponent crypto = i.getInstance(CryptoComponent.class);
-		mac = crypto.getMac();
-		mac.init(crypto.generateSecretKey());
-		macLength = mac.getMacLength();
 	}
 
 	@Test
@@ -64,7 +46,6 @@ public class ConnectionWriterImplTest extends TestCase {
 
 	@Test
 	public void testFrameIsWrittenAtMaxLength() throws Exception {
-		int maxPayloadLength = MAX_FRAME_LENGTH - headerLength - macLength;
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		ConnectionEncrypter e = new NullConnectionEncrypter(out);
 		ConnectionWriter w = new ConnectionWriterImpl(e, mac);
@@ -109,29 +90,4 @@ public class ConnectionWriterImplTest extends TestCase {
 		byte[] actual = out.toByteArray();
 		assertTrue(Arrays.equals(expected, actual));
 	}
-
-	private void writeHeader(byte[] b, long frame, int payload, int padding) {
-		ByteUtils.writeUint32(frame, b, 0);
-		ByteUtils.writeUint16(payload, b, 4);
-		ByteUtils.writeUint16(padding, b, 6);
-	}
-
-	/** A ConnectionEncrypter that performs no encryption. */
-	private static class NullConnectionEncrypter
-	implements ConnectionEncrypter {
-
-		private final OutputStream out;
-
-		private NullConnectionEncrypter(OutputStream out) {
-			this.out = out;
-		}
-
-		public OutputStream getOutputStream() {
-			return out;
-		}
-
-		public void writeMac(byte[] mac) throws IOException {
-			out.write(mac);
-		}
-	}
 }
diff --git a/test/net/sf/briar/transport/NullConnectionDecrypter.java b/test/net/sf/briar/transport/NullConnectionDecrypter.java
new file mode 100644
index 0000000000000000000000000000000000000000..0c6bf77f6e50fa3304f6e72faa9223800bae6e07
--- /dev/null
+++ b/test/net/sf/briar/transport/NullConnectionDecrypter.java
@@ -0,0 +1,29 @@
+package net.sf.briar.transport;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+
+/** A ConnectionDecrypter that performs no decryption. */
+class NullConnectionDecrypter implements ConnectionDecrypter {
+
+	private final InputStream in;
+
+	NullConnectionDecrypter(InputStream in) {
+		this.in = in;
+	}
+
+	public InputStream getInputStream() {
+		return in;
+	}
+
+	public void readMac(byte[] mac) throws IOException {
+		int offset = 0;
+		while(offset < mac.length) {
+			int read = in.read(mac, offset, mac.length - offset);
+			if(read == -1) break;
+			offset += read;
+		}
+		if(offset < mac.length) throw new EOFException();
+	}
+}
diff --git a/test/net/sf/briar/transport/NullConnectionEncrypter.java b/test/net/sf/briar/transport/NullConnectionEncrypter.java
new file mode 100644
index 0000000000000000000000000000000000000000..c9f492d69bd7e99c346b4bdb0afc49c8d0f16a1d
--- /dev/null
+++ b/test/net/sf/briar/transport/NullConnectionEncrypter.java
@@ -0,0 +1,22 @@
+package net.sf.briar.transport;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/** A ConnectionEncrypter that performs no encryption. */
+class NullConnectionEncrypter implements ConnectionEncrypter {
+
+	private final OutputStream out;
+
+	NullConnectionEncrypter(OutputStream out) {
+		this.out = out;
+	}
+
+	public OutputStream getOutputStream() {
+		return out;
+	}
+
+	public void writeMac(byte[] mac) throws IOException {
+		out.write(mac);
+	}
+}
diff --git a/test/net/sf/briar/transport/PaddedConnectionWriterTest.java b/test/net/sf/briar/transport/PaddedConnectionWriterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..2d2a0f22bdc17a34626b3664237ef4dd2602b06f
--- /dev/null
+++ b/test/net/sf/briar/transport/PaddedConnectionWriterTest.java
@@ -0,0 +1,164 @@
+package net.sf.briar.transport;
+
+import static net.sf.briar.api.transport.TransportConstants.MAX_FRAME_LENGTH;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import net.sf.briar.api.transport.ConnectionWriter;
+import net.sf.briar.util.ByteUtils;
+
+import org.junit.Test;
+
+public class PaddedConnectionWriterTest extends TransportTest {
+
+	public PaddedConnectionWriterTest() throws Exception {
+		super();
+	}
+
+	@Test
+	public void testWriteByteDoesNotBlockUntilBufferIsFull() throws Exception {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		ConnectionEncrypter e = new NullConnectionEncrypter(out);
+		ConnectionWriter w = new PaddedConnectionWriter(e, mac);
+		final OutputStream out1 = w.getOutputStream();
+		final CountDownLatch latch = new CountDownLatch(1);
+		final AtomicBoolean finished = new AtomicBoolean(false);
+		final AtomicBoolean failed = new AtomicBoolean(false);
+		new Thread() {
+			@Override
+			public void run() {
+				try {
+					for(int i = 0; i < maxPayloadLength; i++) out1.write(0);
+					finished.set(true);
+				} catch(IOException e) {
+					failed.set(true);
+				}
+				latch.countDown();
+			}
+		}.start();
+		// The wait should not time out
+		assertTrue(latch.await(1, TimeUnit.SECONDS));
+		assertTrue(finished.get());
+		assertFalse(failed.get());
+		// Nothing should have been written
+		assertEquals(0, out.size());
+	}
+
+	@Test
+	public void testWriteByteBlocksWhenBufferIsFull() throws Exception {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		ConnectionEncrypter e = new NullConnectionEncrypter(out);
+		PaddedConnectionWriter w = new PaddedConnectionWriter(e, mac);
+		final OutputStream out1 = w.getOutputStream();
+		final CountDownLatch latch = new CountDownLatch(1);
+		final AtomicBoolean finished = new AtomicBoolean(false);
+		final AtomicBoolean failed = new AtomicBoolean(false);
+		new Thread() {
+			@Override
+			public void run() {
+				try {
+					for(int i = 0; i < maxPayloadLength + 1; i++) out1.write(0);
+					finished.set(true);
+				} catch(IOException e) {
+					failed.set(true);
+				}
+				latch.countDown();
+			}
+		}.start();
+		// The wait should time out
+		assertFalse(latch.await(1, TimeUnit.SECONDS));
+		assertFalse(finished.get());
+		assertFalse(failed.get());
+		// Calling writeFullFrame() should allow the writer to proceed
+		w.writeFullFrame();
+		assertTrue(latch.await(1, TimeUnit.SECONDS));
+		assertTrue(finished.get());
+		assertFalse(failed.get());
+		// A full frame should have been written
+		assertEquals(MAX_FRAME_LENGTH, out.size());
+	}
+
+	@Test
+	public void testWriteArrayDoesNotBlockUntilBufferIsFull() throws Exception {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		ConnectionEncrypter e = new NullConnectionEncrypter(out);
+		ConnectionWriter w = new PaddedConnectionWriter(e, mac);
+		final OutputStream out1 = w.getOutputStream();
+		final CountDownLatch latch = new CountDownLatch(1);
+		final AtomicBoolean finished = new AtomicBoolean(false);
+		final AtomicBoolean failed = new AtomicBoolean(false);
+		new Thread() {
+			@Override
+			public void run() {
+				try {
+					out1.write(new byte[maxPayloadLength]);
+					finished.set(true);
+				} catch(IOException e) {
+					failed.set(true);
+				}
+				latch.countDown();
+			}
+		}.start();
+		// The wait should not time out
+		assertTrue(latch.await(1, TimeUnit.SECONDS));
+		assertTrue(finished.get());
+		assertFalse(failed.get());
+		// Nothing should have been written
+		assertEquals(0, out.size());
+	}
+
+	@Test
+	public void testWriteArrayBlocksWhenBufferIsFull() throws Exception {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		ConnectionEncrypter e = new NullConnectionEncrypter(out);
+		PaddedConnectionWriter w = new PaddedConnectionWriter(e, mac);
+		final OutputStream out1 = w.getOutputStream();
+		final CountDownLatch latch = new CountDownLatch(1);
+		final AtomicBoolean finished = new AtomicBoolean(false);
+		final AtomicBoolean failed = new AtomicBoolean(false);
+		new Thread() {
+			@Override
+			public void run() {
+				try {
+					out1.write(new byte[maxPayloadLength + 1]);
+					finished.set(true);
+				} catch(IOException e) {
+					failed.set(true);
+				}
+				latch.countDown();
+			}
+		}.start();
+		// The wait should time out
+		assertFalse(latch.await(1, TimeUnit.SECONDS));
+		assertFalse(finished.get());
+		assertFalse(failed.get());
+		// Calling writeFullFrame() should allow the writer to proceed
+		w.writeFullFrame();
+		assertTrue(latch.await(1, TimeUnit.SECONDS));
+		assertTrue(finished.get());
+		assertFalse(failed.get());
+		// A full frame should have been written
+		assertEquals(MAX_FRAME_LENGTH, out.size());
+	}
+
+	@Test
+	public void testWriteFullFrameInsertsPadding() throws Exception {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		ConnectionEncrypter e = new NullConnectionEncrypter(out);
+		PaddedConnectionWriter w = new PaddedConnectionWriter(e, mac);
+		w.getOutputStream().write(0);
+		w.writeFullFrame();
+		// A full frame should have been written
+		assertEquals(MAX_FRAME_LENGTH, out.size());
+		// The frame should have a payload length of 1 and padding for the rest
+		byte[] frame = out.toByteArray();
+		assertEquals(0L, ByteUtils.readUint32(frame, 0)); // Frame number
+		assertEquals(1, ByteUtils.readUint16(frame, 4)); // Payload length
+		assertEquals(maxPayloadLength - 1, ByteUtils.readUint16(frame, 6));
+	}
+}
diff --git a/test/net/sf/briar/transport/TransportTest.java b/test/net/sf/briar/transport/TransportTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..5ba5fcbaea7a231dd26623e17b3ae4b8f1b3d0af
--- /dev/null
+++ b/test/net/sf/briar/transport/TransportTest.java
@@ -0,0 +1,35 @@
+package net.sf.briar.transport;
+
+import static net.sf.briar.api.transport.TransportConstants.MAX_FRAME_LENGTH;
+
+import javax.crypto.Mac;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+import net.sf.briar.api.crypto.CryptoComponent;
+import net.sf.briar.crypto.CryptoModule;
+import net.sf.briar.util.ByteUtils;
+import junit.framework.TestCase;
+
+public abstract class TransportTest extends TestCase {
+
+	protected final Mac mac;
+	protected final int headerLength = 8, macLength, maxPayloadLength;
+
+	public TransportTest() throws Exception {
+		super();
+		Injector i = Guice.createInjector(new CryptoModule());
+		CryptoComponent crypto = i.getInstance(CryptoComponent.class);
+		mac = crypto.getMac();
+		mac.init(crypto.generateSecretKey());
+		macLength = mac.getMacLength();
+		maxPayloadLength = MAX_FRAME_LENGTH - headerLength - macLength;
+	}
+
+	static void writeHeader(byte[] b, long frame, int payload, int padding) {
+		ByteUtils.writeUint32(frame, b, 0);
+		ByteUtils.writeUint16(payload, b, 4);
+		ByteUtils.writeUint16(padding, b, 6);
+	}
+}