diff --git a/Makefile b/Makefile
index 1744dfd977af9982ff3cf23218efa79e969a5bf9..2d32c1a1235bfb591594e307e8f2a23a234ffd3b 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,3 @@
-GO=$(shell which go)
 VERSION := $(shell git describe --tag)
 
 .PHONY:
@@ -50,20 +49,20 @@ docs: docs-deps
 check: test fmt-check vet lint staticcheck
 
 test: .PHONY
-	$(GO) test -v ./...
+	go test -v $(shell go list ./... | grep -vE 'ntfy/(test|examples|tools)')
 
 race: .PHONY
-	$(GO) test -race ./...
+	go test -race $(shell go list ./... | grep -vE 'ntfy/(test|examples|tools)')
 
 coverage:
 	mkdir -p build/coverage
-	$(GO) test -race -coverprofile=build/coverage/coverage.txt -covermode=atomic ./...
-	$(GO) tool cover -func build/coverage/coverage.txt
+	go test -race -coverprofile=build/coverage/coverage.txt -covermode=atomic $(shell go list ./... | grep -vE 'ntfy/(test|examples|tools)')
+	go tool cover -func build/coverage/coverage.txt
 
 coverage-html:
 	mkdir -p build/coverage
-	$(GO) test -race -coverprofile=build/coverage/coverage.txt -covermode=atomic ./...
-	$(GO) tool cover -html build/coverage/coverage.txt
+	go test -race -coverprofile=build/coverage/coverage.txt -covermode=atomic $(shell go list ./... | grep -vE 'ntfy/(test|examples|tools)')
+	go tool cover -html build/coverage/coverage.txt
 
 coverage-upload:
 	cd build/coverage && (curl -s https://codecov.io/bash | bash)
@@ -78,17 +77,17 @@ fmt-check:
 	test -z $(shell gofmt -l .)
 
 vet:
-	$(GO) vet ./...
+	go vet ./...
 
 lint:
-	which golint || $(GO) get -u golang.org/x/lint/golint
-	$(GO) list ./... | grep -v /vendor/ | xargs -L1 golint -set_exit_status
+	which golint || go get -u golang.org/x/lint/golint
+	go list ./... | grep -v /vendor/ | xargs -L1 golint -set_exit_status
 
 staticcheck: .PHONY
 	rm -rf build/staticcheck
 	which staticcheck || go install honnef.co/go/tools/cmd/staticcheck@latest
 	mkdir -p build/staticcheck
-	ln -s "$(GO)" build/staticcheck/go
+	ln -s "go" build/staticcheck/go
 	PATH="$(PWD)/build/staticcheck:$(PATH)" staticcheck ./...
 	rm -rf build/staticcheck
 
@@ -108,7 +107,7 @@ build-snapshot: build-deps
 build-simple: clean
 	mkdir -p dist/ntfy_linux_amd64
 	export CGO_ENABLED=1
-	$(GO) build \
+	go build \
 		-o dist/ntfy_linux_amd64/ntfy \
 		-tags sqlite_omit_load_extension,osusergo,netgo \
 		-ldflags \
diff --git a/client/client.go b/client/client.go
index 455a9aa6bf3e53ab1a7a0ec1dfd42bcb65c6ec16..abcfecf703fea5ede844d9b062904b3e070202d8 100644
--- a/client/client.go
+++ b/client/client.go
@@ -35,7 +35,7 @@ type Client struct {
 }
 
 // Message is a struct that represents a ntfy message
-type Message struct {
+type Message struct { // TODO combine with server.message
 	ID       string
 	Event    string
 	Time     int64
@@ -60,7 +60,7 @@ type subscription struct {
 // New creates a new Client using a given Config
 func New(config *Config) *Client {
 	return &Client{
-		Messages:      make(chan *Message),
+		Messages:      make(chan *Message, 50), // Allow reading a few messages
 		config:        config,
 		subscriptions: make(map[string]*subscription),
 	}
@@ -237,7 +237,9 @@ func performSubscribeRequest(ctx context.Context, msgChan chan *Message, topicUR
 		if err != nil {
 			return err
 		}
-		msgChan <- m
+		if m.Event == MessageEvent {
+			msgChan <- m
+		}
 	}
 	return nil
 }
diff --git a/client/client_test.go b/client/client_test.go
index b010fd9b8fb136711db7559fead660c4f3fd82de..aca16748642249a671d10818874662e99319b42a 100644
--- a/client/client_test.go
+++ b/client/client_test.go
@@ -1,42 +1,78 @@
 package client_test
 
 import (
+	"fmt"
 	"github.com/stretchr/testify/require"
 	"heckel.io/ntfy/client"
-	"heckel.io/ntfy/server"
-	"net/http"
+	"heckel.io/ntfy/test"
 	"testing"
 	"time"
 )
 
-func TestClient_Publish(t *testing.T) {
-	s := startTestServer(t)
-	defer s.Stop()
-	c := client.New(newTestConfig())
+func TestClient_Publish_Subscribe(t *testing.T) {
+	s, port := test.StartServer(t)
+	defer test.StopServer(t, s, port)
+	c := client.New(newTestConfig(port))
 
-	time.Sleep(time.Second) // FIXME Wait for port up
+	subscriptionID := c.Subscribe("mytopic")
+	time.Sleep(time.Second)
 
-	_, err := c.Publish("mytopic", "some message")
+	msg, err := c.Publish("mytopic", "some message")
 	require.Nil(t, err)
+	require.Equal(t, "some message", msg.Message)
+
+	msg, err = c.Publish("mytopic", "some other message",
+		client.WithTitle("some title"),
+		client.WithPriority("high"),
+		client.WithTags([]string{"tag1", "tag 2"}))
+	require.Nil(t, err)
+	require.Equal(t, "some other message", msg.Message)
+	require.Equal(t, "some title", msg.Title)
+	require.Equal(t, []string{"tag1", "tag 2"}, msg.Tags)
+	require.Equal(t, 4, msg.Priority)
+
+	msg, err = c.Publish("mytopic", "some delayed message",
+		client.WithDelay("25 hours"))
+	require.Nil(t, err)
+	require.Equal(t, "some delayed message", msg.Message)
+	require.True(t, time.Now().Add(24*time.Hour).Unix() < msg.Time)
+
+	msg = nextMessage(c)
+	require.NotNil(t, msg)
+	require.Equal(t, "some message", msg.Message)
+
+	msg = nextMessage(c)
+	require.NotNil(t, msg)
+	require.Equal(t, "some other message", msg.Message)
+	require.Equal(t, "some title", msg.Title)
+	require.Equal(t, []string{"tag1", "tag 2"}, msg.Tags)
+	require.Equal(t, 4, msg.Priority)
+
+	msg = nextMessage(c)
+	require.Nil(t, msg)
+
+	c.Unsubscribe(subscriptionID)
+	time.Sleep(200 * time.Millisecond)
+
+	msg, err = c.Publish("mytopic", "a message that won't be received")
+	require.Nil(t, err)
+	require.Equal(t, "a message that won't be received", msg.Message)
+
+	msg = nextMessage(c)
+	require.Nil(t, msg)
 }
 
-func newTestConfig() *client.Config {
+func newTestConfig(port int) *client.Config {
 	c := client.NewConfig()
-	c.DefaultHost = "http://127.0.0.1:12345"
+	c.DefaultHost = fmt.Sprintf("http://127.0.0.1:%d", port)
 	return c
 }
 
-func startTestServer(t *testing.T) *server.Server {
-	conf := server.NewConfig()
-	conf.ListenHTTP = ":12345"
-	s, err := server.New(conf)
-	if err != nil {
-		t.Fatal(err)
+func nextMessage(c *client.Client) *client.Message {
+	select {
+	case m := <-c.Messages:
+		return m
+	default:
+		return nil
 	}
-	go func() {
-		if err := s.Run(); err != nil && err != http.ErrServerClosed {
-			panic(err) // 'go vet' complains about 't.Fatal(err)'
-		}
-	}()
-	return s
 }
diff --git a/cmd/subscribe.go b/cmd/subscribe.go
index 12db37c096a9e5f75112c55a592e2338faf5e73b..0c9447c26bc823e85a9df69fb3ee7c1f28ca8e47 100644
--- a/cmd/subscribe.go
+++ b/cmd/subscribe.go
@@ -158,9 +158,6 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic,
 }
 
 func printMessageOrRunCommand(c *cli.Context, m *client.Message, command string) {
-	if m.Event != client.MessageEvent {
-		return
-	}
 	if command != "" {
 		runCommand(c, command, m)
 	} else {
diff --git a/server/server.go b/server/server.go
index 4d31215d7770c745448852d29b584df694626f48..1a02f3a8fe4a71fc19b438fe38aeb83cc3f28018 100644
--- a/server/server.go
+++ b/server/server.go
@@ -191,9 +191,6 @@ func createFirebaseSubscriber(conf *Config) (subscriber, error) {
 // Run executes the main server. It listens on HTTP (+ HTTPS, if configured), and starts
 // a manager go routine to print stats and prune messages.
 func (s *Server) Run() error {
-	go s.runManager()
-	go s.runAtSender()
-	go s.runFirebaseKeepliver()
 	listenStr := fmt.Sprintf("%s/http", s.config.ListenHTTP)
 	if s.config.ListenHTTPS != "" {
 		listenStr += fmt.Sprintf(" %s/https", s.config.ListenHTTPS)
@@ -214,6 +211,9 @@ func (s *Server) Run() error {
 		}()
 	}
 	s.mu.Unlock()
+	go s.runManager()
+	go s.runAtSender()
+	go s.runFirebaseKeepliver()
 	return <-errChan
 }
 
diff --git a/test/server.go b/test/server.go
new file mode 100644
index 0000000000000000000000000000000000000000..5c5e4b310dfc0312b4a6ac0fee6878ea556f52e0
--- /dev/null
+++ b/test/server.go
@@ -0,0 +1,38 @@
+package test
+
+import (
+	"fmt"
+	"heckel.io/ntfy/server"
+	"math/rand"
+	"net/http"
+	"testing"
+	"time"
+)
+
+func init() {
+	rand.Seed(time.Now().Unix())
+}
+
+// StartServer starts a server.Server with a random port and waits for the server to be up
+func StartServer(t *testing.T) (*server.Server, int) {
+	port := 10000 + rand.Intn(20000)
+	conf := server.NewConfig()
+	conf.ListenHTTP = fmt.Sprintf(":%d", port)
+	s, err := server.New(conf)
+	if err != nil {
+		t.Fatal(err)
+	}
+	go func() {
+		if err := s.Run(); err != nil && err != http.ErrServerClosed {
+			panic(err) // 'go vet' complains about 't.Fatal(err)'
+		}
+	}()
+	WaitForPortUp(t, port)
+	return s, port
+}
+
+// StopServer stops the test server and waits for the port to be down
+func StopServer(t *testing.T, s *server.Server, port int) {
+	s.Stop()
+	WaitForPortDown(t, port)
+}
diff --git a/test/test.go b/test/test.go
new file mode 100644
index 0000000000000000000000000000000000000000..837b13962e4fe869840caa63cb09f2521a54e417
--- /dev/null
+++ b/test/test.go
@@ -0,0 +1,3 @@
+// Package test provides test helpers for unit and integration tests.
+// This code is not meant to be used outside of tests.
+package test
diff --git a/test/util.go b/test/util.go
new file mode 100644
index 0000000000000000000000000000000000000000..dcd6a96093d411847b195469f26eac5877cf4178
--- /dev/null
+++ b/test/util.go
@@ -0,0 +1,44 @@
+package test
+
+import (
+	"net"
+	"strconv"
+	"testing"
+	"time"
+)
+
+// WaitForPortUp waits up to 7s for a port to come up and fails t if that fails
+func WaitForPortUp(t *testing.T, port int) {
+	success := false
+	for i := 0; i < 500; i++ {
+		startTime := time.Now()
+		conn, _ := net.DialTimeout("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port)), 10*time.Millisecond)
+		if conn != nil {
+			success = true
+			conn.Close()
+			break
+		}
+		if time.Since(startTime) < 10*time.Millisecond {
+			time.Sleep(10*time.Millisecond - time.Since(startTime))
+		}
+	}
+	if !success {
+		t.Fatalf("Failed waiting for port %d to be UP", port)
+	}
+}
+
+// WaitForPortDown waits up to 5s for a port to come down and fails t if that fails
+func WaitForPortDown(t *testing.T, port int) {
+	success := false
+	for i := 0; i < 100; i++ {
+		conn, _ := net.DialTimeout("tcp", net.JoinHostPort("", strconv.Itoa(port)), 50*time.Millisecond)
+		if conn == nil {
+			success = true
+			break
+		}
+		conn.Close()
+	}
+	if !success {
+		t.Fatalf("Failed waiting for port %d to be DOWN", port)
+	}
+}