diff --git a/bindings/dummy.go b/bindings/dummy.go
new file mode 100644
index 0000000000000000000000000000000000000000..8cbaa84dc6a26f8b28765b5334b5be1bd6fe5df3
--- /dev/null
+++ b/bindings/dummy.go
@@ -0,0 +1,29 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                           //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package bindings
+
+import (
+	"gitlab.com/elixxir/client/dummy"
+	"time"
+)
+
+// StartDummyTraffic starts sending dummy traffic. The maxNumMessages is the
+// upper bound of the random number of messages sent each send. avgSendDeltaMS
+// is the average duration, in milliseconds, to wait between sends. Sends occur
+// every avgSendDeltaMS +/- a random duration with an upper bound of
+// randomRangeMS.
+func StartDummyTraffic(client *Client, maxNumMessages, avgSendDeltaMS,
+	randomRangeMS int) error {
+	avgSendDelta := time.Duration(avgSendDeltaMS) * time.Millisecond
+	randomRange := time.Duration(randomRangeMS) * time.Millisecond
+
+	m := dummy.NewManager(
+		maxNumMessages, avgSendDelta, randomRange, &client.api)
+
+	return client.api.AddService(m.StartDummyTraffic)
+}
diff --git a/dummy/manager.go b/dummy/manager.go
new file mode 100644
index 0000000000000000000000000000000000000000..edf0b3ac3d1ece2e8fda7ab81376d9b162241a8d
--- /dev/null
+++ b/dummy/manager.go
@@ -0,0 +1,75 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                           //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+// Package dummy allows for the sending of dummy messages to dummy recipients
+// via SendCmix at randomly generated intervals.
+
+package dummy
+
+import (
+	"gitlab.com/elixxir/client/api"
+	"gitlab.com/elixxir/client/interfaces"
+	"gitlab.com/elixxir/client/stoppable"
+	"gitlab.com/elixxir/client/storage"
+	"gitlab.com/elixxir/crypto/fastRNG"
+	"time"
+)
+
+const (
+	dummyTrafficStoppableName = "DummyTraffic"
+)
+
+// Manager manages the sending of dummy messages.
+type Manager struct {
+	// The maximum number of messages to send each send
+	maxNumMessages int
+
+	// Average duration to wait between message sends
+	avgSendDelta time.Duration
+
+	// Upper limit for random duration that modified avgSendDelta
+	randomRange time.Duration
+
+	// Client interfaces
+	client *api.Client
+	store  *storage.Session
+	net    interfaces.NetworkManager
+	rng    *fastRNG.StreamGenerator
+}
+
+// NewManager creates a new dummy Manager with the specified average send delta
+// and the range used for generating random durations.
+func NewManager(maxNumMessages int, avgSendDelta, randomRange time.Duration,
+	client *api.Client) *Manager {
+	return newManager(maxNumMessages, avgSendDelta, randomRange, client,
+		client.GetStorage(), client.GetNetworkInterface(), client.GetRng())
+}
+
+// newManager builds a new dummy Manager from fields explicitly passed in. This
+// function is a helper function for NewManager to make it easier to test.
+func newManager(maxNumMessages int, avgSendDelta, randomRange time.Duration,
+	client *api.Client, store *storage.Session, net interfaces.NetworkManager,
+	rng *fastRNG.StreamGenerator) *Manager {
+	return &Manager{
+		maxNumMessages: maxNumMessages,
+		avgSendDelta:   avgSendDelta,
+		randomRange:    randomRange,
+		client:         client,
+		store:          store,
+		net:            net,
+		rng:            rng,
+	}
+}
+
+// StartDummyTraffic starts the process of sending dummy traffic. This function
+// matches the api.Service type.
+func (m *Manager) StartDummyTraffic() (stoppable.Stoppable, error) {
+	stop := stoppable.NewSingle(dummyTrafficStoppableName)
+	go m.sendThread(stop)
+
+	return stop, nil
+}
diff --git a/dummy/manager_test.go b/dummy/manager_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..7f8ec99d037dd14576951237abb6113518d89ab9
--- /dev/null
+++ b/dummy/manager_test.go
@@ -0,0 +1,84 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                           //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package dummy
+
+import (
+	"reflect"
+	"testing"
+	"time"
+)
+
+// Tests that newManager returns the expected Manager.
+func Test_newManager(t *testing.T) {
+	expected := &Manager{
+		maxNumMessages: 10,
+		avgSendDelta:   time.Minute,
+		randomRange:    time.Second,
+	}
+
+	received := newManager(expected.maxNumMessages, expected.avgSendDelta,
+		expected.randomRange, nil, nil, nil, nil)
+
+	if !reflect.DeepEqual(expected, received) {
+		t.Errorf("New manager does not match expected."+
+			"\nexpected: %+v\nreceived: %+v", expected, received)
+	}
+}
+
+// Tests that Manager.StartDummyTraffic sends dummy messages and that it stops
+// when the stoppable is closed.
+func TestManager_StartDummyTraffic(t *testing.T) {
+	m := newTestManager(10, 50*time.Millisecond, 10*time.Millisecond, false, t)
+
+	stop, err := m.StartDummyTraffic()
+	if err != nil {
+		t.Errorf("StartDummyTraffic returned an error: %+v", err)
+	}
+
+	msgChan := make(chan bool)
+	go func() {
+		for m.net.(*testNetworkManager).GetMsgListLen() == 0 {
+			time.Sleep(5 * time.Millisecond)
+		}
+		msgChan <- true
+	}()
+
+	var numReceived int
+	select {
+	case <-time.NewTimer(3 * m.avgSendDelta).C:
+		t.Errorf("Timed out after %s waiting for messages to be sent.",
+			3*m.avgSendDelta)
+	case <-msgChan:
+		numReceived += m.net.(*testNetworkManager).GetMsgListLen()
+	}
+
+	err = stop.Close()
+	if err != nil {
+		t.Errorf("Failed to close stoppable: %+v", err)
+	}
+
+	time.Sleep(10 * time.Millisecond)
+	if !stop.IsStopped() {
+		t.Error("Stoppable never stopped.")
+	}
+
+	msgChan = make(chan bool)
+	go func() {
+		for m.net.(*testNetworkManager).GetMsgListLen() == numReceived {
+			time.Sleep(5 * time.Millisecond)
+		}
+		msgChan <- true
+	}()
+
+	select {
+	case <-time.NewTimer(3 * m.avgSendDelta).C:
+
+	case <-msgChan:
+		t.Error("Received new messages after stoppable was stopped.")
+	}
+}
diff --git a/dummy/random.go b/dummy/random.go
new file mode 100644
index 0000000000000000000000000000000000000000..2327ddf6c7d9b978ecf6e07e34f53b733155c87e
--- /dev/null
+++ b/dummy/random.go
@@ -0,0 +1,87 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                           //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package dummy
+
+import (
+	"encoding/binary"
+	"github.com/pkg/errors"
+	"gitlab.com/elixxir/primitives/format"
+	"gitlab.com/xx_network/crypto/csprng"
+	"time"
+) // Error messages.
+const (
+	payloadSizeRngErr = "failed to generate random payload size: %+v"
+)
+
+// intRng returns, as an int, a non-negative, non-zero random number in [1, n)
+// from the csprng.Source.
+func intRng(n int, rng csprng.Source) (int, error) {
+	v, err := csprng.Generate(8, rng)
+	if err != nil {
+		return 0, err
+	}
+
+	return int(binary.LittleEndian.Uint64(v)%uint64(n-1)) + 1, nil
+}
+
+// durationRng returns a duration that is the base duration plus or minus a
+// random duration of max randomRange.
+func durationRng(base, randomRange time.Duration, rng csprng.Source) (
+	time.Duration, error) {
+	delta, err := intRng(int(2*randomRange), rng)
+	if err != nil {
+		return 0, err
+	}
+
+	return base + randomRange - time.Duration(delta), nil
+}
+
+// newRandomPayload generates a random payload of a random length.
+func newRandomPayload(maxPayloadSize int, rng csprng.Source) ([]byte, error) {
+	// Generate random payload size
+	randomPayloadSize, err := intRng(maxPayloadSize, rng)
+	if err != nil {
+		return nil, errors.Errorf(payloadSizeRngErr, err)
+	}
+
+	randomMsg, err := csprng.Generate(randomPayloadSize, rng)
+	if err != nil {
+		return nil, err
+	}
+
+	return randomMsg, nil
+}
+
+// newRandomFingerprint generates a random format.Fingerprint.
+func newRandomFingerprint(rng csprng.Source) (format.Fingerprint, error) {
+	fingerprintBytes, err := csprng.Generate(format.KeyFPLen, rng)
+	if err != nil {
+		return format.Fingerprint{}, err
+	}
+
+	// Create new fingerprint from bytes
+	fingerprint := format.NewFingerprint(fingerprintBytes)
+
+	// Set the first bit to be 0 to comply with the cMix group
+	fingerprint[0] &= 0x7F
+
+	return fingerprint, nil
+}
+
+// newRandomMAC generates a random MAC.
+func newRandomMAC(rng csprng.Source) ([]byte, error) {
+	mac, err := csprng.Generate(format.MacLen, rng)
+	if err != nil {
+		return nil, err
+	}
+
+	// Set the first bit to be 0 to comply with the cMix group
+	mac[0] &= 0x7F
+
+	return mac, nil
+}
diff --git a/dummy/random_test.go b/dummy/random_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..661986a0416993e211209d009a023c451dd3ff60
--- /dev/null
+++ b/dummy/random_test.go
@@ -0,0 +1,188 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                           //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package dummy
+
+import (
+	"encoding/base64"
+	"testing"
+	"time"
+)
+
+// Consistency test: tests that intRng returns the expected int when using a
+// PRNG and that the result is not larger than the max.
+func Test_intRng_Consistency(t *testing.T) {
+	expectedInts := []int{15, 1, 35, 13, 42, 52, 57, 3, 48}
+
+	prng := NewPrng(42)
+	max := 64
+
+	for i, expected := range expectedInts {
+		v, err := intRng(max, prng)
+		if err != nil {
+			t.Errorf("intRng returned an error (%d): %+v", i, err)
+		}
+
+		if v != expected {
+			t.Errorf("New int #%d does not match expected."+
+				"\nexpected: %d\nreceived: %d", i, expected, v)
+		}
+
+		// Ensure that the int is in range
+		if v > max || v < 1 {
+			t.Errorf("Int #%d not within range."+
+				"\nexpected: %d < d < %d\nreceived: %d", i, 0, max, v)
+		}
+	}
+}
+
+// Consistency test: tests that durationRng returns the expected int when using
+// a PRNG and that the result is within the allowed range.
+func Test_durationRng_Consistency(t *testing.T) {
+	expectedDurations := []time.Duration{
+		61460632462, 69300060600, 46066982720, 68493307162, 45820762465,
+		56472560211, 68610237306, 45503877311, 63543617747,
+	}
+
+	prng := NewPrng(42)
+	base, randomRange := time.Minute, 15*time.Second
+
+	for i, expected := range expectedDurations {
+		v, err := durationRng(base, randomRange, prng)
+		if err != nil {
+			t.Errorf("durationRng returned an error (%d): %+v", i, err)
+		}
+
+		if v != expected {
+			t.Errorf("New duration #%d does not match expected."+
+				"\nexpected: %s\nreceived: %s", i, expected, v)
+		}
+
+		// Ensure that the duration is within range
+		if v > base+randomRange || v < base-randomRange {
+			t.Errorf("Duration #%d is not in range."+
+				"\nexpected: %s < d < %s\nreceived: %s", i, base-randomRange,
+				base+randomRange, v)
+		}
+	}
+}
+
+// Consistency test: tests that newRandomPayload returns the expected payload
+// when using a PRNG and that the result is not larger than the max payload.
+func Test_newRandomPayload_Consistency(t *testing.T) {
+	expectedPayloads := []string{
+		"l7ufS7Ry6J9bFITyUgnJ",
+		"Ut/Xm012Qpthegyfnw07pVsMwNYUTIiFNQ==",
+		"CD9h",
+		"GSnh",
+		"joE=",
+		"uoQ+6NY+jE/+HOvqVG2PrBPdGqwEzi6ih3xVec+ix44bC6+uiBuCpw==",
+		"qkNGWnhiBhaXiu0M48bE8657w+BJW1cS/v2+DBAoh+EA2s0tiF9pLLYH2gChHBxwcec=",
+		"suEpcF4nPwXJIyaCjisFbg==",
+		"R/3zREEO1MEWAj+o41drb+0n/4l0usDK/ZrQVpKxNhnnOJZN/ceejVNDc2Yc/WbXTw==",
+		"bkt1IQ==",
+	}
+
+	prng := NewPrng(42)
+	maxPayloadSize := 64
+
+	for i, expected := range expectedPayloads {
+		payload, err := newRandomPayload(maxPayloadSize, prng)
+		if err != nil {
+			t.Errorf("newRandomPayload returned an error (%d): %+v", i, err)
+		}
+
+		payloadString := base64.StdEncoding.EncodeToString(payload)
+
+		if payloadString != expected {
+			t.Errorf("New payload #%d does not match expected."+
+				"\nexpected: %s\nreceived: %s", i, expected, payloadString)
+		}
+
+		// Ensure that the payload is not larger than the max size
+		if len(payload) > maxPayloadSize {
+			t.Errorf("Length of payload #%d longer than max allowed."+
+				"\nexpected: <%d\nreceived: %d", i, maxPayloadSize, len(payload))
+		}
+	}
+}
+
+// Consistency test: tests that newRandomFingerprint returns the expected
+// fingerprints when using a PRNG. Also tests that the first bit is zero.
+func Test_newRandomFingerprint_Consistency(t *testing.T) {
+	expectedFingerprints := []string{
+		"U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVI=",
+		"X9ebTXZCm2F6DJ+fDTulWwzA1hRMiIU1hBrL4HCbB1g=",
+		"CD9h03W8ArQd9PkZKeGP2p5vguVOdI6B555LvW/jTNw=",
+		"OoQ+6NY+jE/+HOvqVG2PrBPdGqwEzi6ih3xVec+ix44=",
+		"GwuvrogbgqdREIpC7TyQPKpDRlp4YgYWl4rtDOPGxPM=",
+		"LnvD4ElbVxL+/b4MECiH4QDazS2IX2kstgfaAKEcHHA=",
+		"ceeWotwtwlpbdLLhKXBeJz8FySMmgo4rBW44F2WOEGE=",
+		"SYlH/fNEQQ7UwRYCP6jjV2tv7Sf/iXS6wMr9mtBWkrE=",
+		"NhnnOJZN/ceejVNDc2Yc/WbXT+weG4lJGrcjbkt1IWI=",
+		"EM8r60LDyicyhWDxqsBnzqbov0bUqytGgEAsX7KCDog=",
+	}
+
+	prng := NewPrng(42)
+
+	for i, expected := range expectedFingerprints {
+		fp, err := newRandomFingerprint(prng)
+		if err != nil {
+			t.Errorf("newRandomFingerprint returned an error (%d): %+v", i, err)
+		}
+
+		if fp.String() != expected {
+			t.Errorf("New fingerprint #%d does not match expected."+
+				"\nexpected: %s\nreceived: %s", i, expected, fp)
+		}
+
+		// Ensure that the first bit is zero
+		if fp[0]>>7 != 0 {
+			t.Errorf("First bit of fingerprint #%d is not 0."+
+				"\nexpected: %d\nreceived: %d", i, 0, fp[0]>>7)
+		}
+	}
+}
+
+// Consistency test: tests that newRandomMAC returns the expected MAC when using
+// a PRNG. Also tests that the first bit is zero.
+func Test_newRandomMAC_Consistency(t *testing.T) {
+	expectedMACs := []string{
+		"U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVI=",
+		"X9ebTXZCm2F6DJ+fDTulWwzA1hRMiIU1hBrL4HCbB1g=",
+		"CD9h03W8ArQd9PkZKeGP2p5vguVOdI6B555LvW/jTNw=",
+		"OoQ+6NY+jE/+HOvqVG2PrBPdGqwEzi6ih3xVec+ix44=",
+		"GwuvrogbgqdREIpC7TyQPKpDRlp4YgYWl4rtDOPGxPM=",
+		"LnvD4ElbVxL+/b4MECiH4QDazS2IX2kstgfaAKEcHHA=",
+		"ceeWotwtwlpbdLLhKXBeJz8FySMmgo4rBW44F2WOEGE=",
+		"SYlH/fNEQQ7UwRYCP6jjV2tv7Sf/iXS6wMr9mtBWkrE=",
+		"NhnnOJZN/ceejVNDc2Yc/WbXT+weG4lJGrcjbkt1IWI=",
+		"EM8r60LDyicyhWDxqsBnzqbov0bUqytGgEAsX7KCDog=",
+	}
+
+	prng := NewPrng(42)
+
+	for i, expected := range expectedMACs {
+		mac, err := newRandomMAC(prng)
+		if err != nil {
+			t.Errorf("newRandomMAC returned an error (%d): %+v", i, err)
+		}
+
+		macString := base64.StdEncoding.EncodeToString(mac)
+
+		if macString != expected {
+			t.Errorf("New MAC #%d does not match expected."+
+				"\nexpected: %s\nreceived: %s", i, expected, macString)
+		}
+
+		// Ensure that the first bit is zero
+		if mac[0]>>7 != 0 {
+			t.Errorf("First bit of MAC #%d is not 0."+
+				"\nexpected: %d\nreceived: %d", i, 0, mac[0]>>7)
+		}
+	}
+}
diff --git a/dummy/send.go b/dummy/send.go
new file mode 100644
index 0000000000000000000000000000000000000000..6cde639e97f776b485d599d5a2659771751e487f
--- /dev/null
+++ b/dummy/send.go
@@ -0,0 +1,164 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                           //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package dummy
+
+import (
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/interfaces/params"
+	"gitlab.com/elixxir/client/stoppable"
+	"gitlab.com/elixxir/primitives/format"
+	"gitlab.com/xx_network/crypto/csprng"
+	"gitlab.com/xx_network/primitives/id"
+	"sync"
+	"sync/atomic"
+	"time"
+)
+
+// Error messages.
+const (
+	numMsgsRngErr     = "failed to generate random number of messages to send: %+v"
+	payloadRngErr     = "failed to generate random payload: %+v"
+	recipientRngErr   = "failed to generate random recipient: %+v"
+	fingerprintRngErr = "failed to generate random fingerprint: %+v"
+	macRngErr         = "failed to generate random MAC: %+v"
+)
+
+// sendThread is a thread that sends the dummy messages at random intervals.
+func (m *Manager) sendThread(stop *stoppable.Single) {
+	jww.DEBUG.Print("Starting dummy traffic sending thread.")
+
+	timer := m.randomTimer()
+
+	for {
+		select {
+		case <-stop.Quit():
+			jww.DEBUG.Print("Stopping dummy traffic sending thread: stoppable " +
+				"triggered")
+			stop.ToStopped()
+			return
+		case <-timer.C:
+			timer = m.randomTimer()
+
+			// Get list of random messages and recipients
+			rng := m.rng.GetStream()
+			msgs, err := m.newRandomMessages(rng)
+			if err != nil {
+				jww.FATAL.Panicf("Failed to generate dummy messages: %+v", err)
+			}
+			rng.Close()
+
+			err = m.sendMessages(msgs)
+			if err != nil {
+				jww.FATAL.Panicf("Failed to send dummy messages: %+v", err)
+			}
+		}
+	}
+}
+
+// sendMessages generates and sends random messages.
+func (m *Manager) sendMessages(msgs map[id.ID]format.Message) error {
+	var sent, i int64
+	var wg sync.WaitGroup
+
+	for recipient, msg := range msgs {
+		wg.Add(1)
+
+		go func(i int64, recipient id.ID, msg format.Message) {
+			_, _, err := m.net.SendCMIX(msg, &recipient, params.GetDefaultCMIX())
+			if err != nil {
+				jww.WARN.Printf("failed to send dummy message %d/%d: %+v",
+					i, len(msgs), err)
+			} else {
+				atomic.AddInt64(&sent, 1)
+			}
+
+			wg.Done()
+		}(i, recipient, msg)
+
+		i++
+	}
+
+	wg.Wait()
+
+	jww.INFO.Printf("Sent %d/%d dummy messages.", sent, len(msgs))
+
+	return nil
+}
+
+// newRandomMessages returns a map of a random recipients and random messages of
+// a randomly generated length in [1, Manager.maxNumMessages].
+func (m *Manager) newRandomMessages(rng csprng.Source) (
+	map[id.ID]format.Message, error) {
+	numMessages, err := intRng(m.maxNumMessages+1, rng)
+	if err != nil {
+		return nil, errors.Errorf(numMsgsRngErr, err)
+	}
+
+	msgs := make(map[id.ID]format.Message, numMessages)
+
+	for i := 0; i < numMessages; i++ {
+		// Generate random recipient
+		recipient, err := id.NewRandomID(rng, id.User)
+		if err != nil {
+			return nil, errors.Errorf(recipientRngErr, err)
+		}
+
+		msgs[*recipient], err = m.newRandomCmixMessage(rng)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return msgs, nil
+}
+
+// newRandomCmixMessage returns a new cMix message filled with a randomly
+// generated payload, fingerprint, and MAC.
+func (m *Manager) newRandomCmixMessage(rng csprng.Source) (format.Message, error) {
+	// Create new empty cMix message
+	cMixMsg := format.NewMessage(m.store.Cmix().GetGroup().GetP().ByteLen())
+
+	// Generate random message
+	randomMsg, err := newRandomPayload(cMixMsg.ContentsSize(), rng)
+	if err != nil {
+		return format.Message{}, errors.Errorf(payloadRngErr, err)
+	}
+
+	// Generate random fingerprint
+	fingerprint, err := newRandomFingerprint(rng)
+	if err != nil {
+		return format.Message{}, errors.Errorf(fingerprintRngErr, err)
+	}
+
+	// Generate random MAC
+	mac, err := newRandomMAC(rng)
+	if err != nil {
+		return format.Message{}, errors.Errorf(macRngErr, err)
+	}
+
+	// Set contents, fingerprint, and MAC, of the cMix message
+	cMixMsg.SetContents(randomMsg)
+	cMixMsg.SetKeyFP(fingerprint)
+	cMixMsg.SetMac(mac)
+
+	return cMixMsg, nil
+}
+
+// randomTimer generates a timer that will trigger after a random duration.
+func (m *Manager) randomTimer() *time.Timer {
+	rng := m.rng.GetStream()
+
+	duration, err := durationRng(m.avgSendDelta, m.randomRange, rng)
+	if err != nil {
+		jww.FATAL.Panicf("Failed to generate random duration to wait to send "+
+			"dummy messages: %+v", err)
+	}
+
+	return time.NewTimer(duration)
+}
diff --git a/dummy/send_test.go b/dummy/send_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..acf08cb558ba02fbf711537ce67a696afa455836
--- /dev/null
+++ b/dummy/send_test.go
@@ -0,0 +1,169 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                           //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package dummy
+
+import (
+	"bytes"
+	"encoding/base64"
+	"gitlab.com/elixxir/client/stoppable"
+	"gitlab.com/elixxir/primitives/format"
+	"gitlab.com/xx_network/primitives/id"
+	"reflect"
+	"testing"
+	"time"
+)
+
+// Tests that Manager.sendThread sends multiple sets of messages.
+func TestManager_sendThread(t *testing.T) {
+	m := newTestManager(10, 50*time.Millisecond, 10*time.Millisecond, false, t)
+
+	stop := stoppable.NewSingle("sendThreadTest")
+	go m.sendThread(stop)
+
+	msgChan := make(chan bool, 10)
+	go func() {
+		var numReceived int
+		for i := 0; i < 2; i++ {
+			for m.net.(*testNetworkManager).GetMsgListLen() == numReceived {
+				time.Sleep(5 * time.Millisecond)
+			}
+			numReceived = m.net.(*testNetworkManager).GetMsgListLen()
+			msgChan <- true
+		}
+	}()
+
+	var numReceived int
+	select {
+	case <-time.NewTimer(3 * m.avgSendDelta).C:
+		t.Errorf("Timed out after %s waiting for messages to be sent.",
+			3*m.avgSendDelta)
+	case <-msgChan:
+		numReceived += m.net.(*testNetworkManager).GetMsgListLen()
+	}
+
+	select {
+	case <-time.NewTimer(3 * m.avgSendDelta).C:
+		t.Errorf("Timed out after %s waiting for messages to be sent.",
+			3*m.avgSendDelta)
+	case <-msgChan:
+		if m.net.(*testNetworkManager).GetMsgListLen() <= numReceived {
+			t.Errorf("Failed to receive second send."+
+				"\nmessages on last receive: %d\nmessages on this receive: %d",
+				numReceived, m.net.(*testNetworkManager).GetMsgListLen())
+		}
+	}
+
+	err := stop.Close()
+	if err != nil {
+		t.Errorf("Failed to close stoppable: %+v", err)
+	}
+
+	time.Sleep(10 * time.Millisecond)
+	if !stop.IsStopped() {
+		t.Error("Stoppable never stopped.")
+	}
+}
+
+// Tests that Manager.sendMessages sends all the messages with the correct
+// recipient.
+func TestManager_sendMessages(t *testing.T) {
+	m := newTestManager(100, 0, 0, false, t)
+	prng := NewPrng(42)
+
+	// Generate map of recipients and messages
+	msgs := make(map[id.ID]format.Message, m.maxNumMessages)
+	for i := 0; i < m.maxNumMessages; i++ {
+		recipient, err := id.NewRandomID(prng, id.User)
+		if err != nil {
+			t.Errorf("Failed to generate random recipient ID (%d): %+v", i, err)
+		}
+
+		msg, err := m.newRandomCmixMessage(prng)
+		if err != nil {
+			t.Errorf("Failed to generate random cMix message (%d): %+v", i, err)
+		}
+
+		msgs[*recipient] = msg
+	}
+
+	// Send the messages
+	err := m.sendMessages(msgs)
+	if err != nil {
+		t.Errorf("sendMessages returned an error: %+v", err)
+	}
+
+	// Get sent messages
+	receivedMsgs := m.net.(*testNetworkManager).GetMsgList()
+
+	// Test that all messages were received
+	if len(receivedMsgs) != len(msgs) {
+		t.Errorf("Failed to received all sent messages."+
+			"\nexpected: %d\nreceived: %d", len(msgs), len(receivedMsgs))
+	}
+
+	// Test that all messages were received for the correct recipient
+	for recipient, msg := range msgs {
+		receivedMsg, exists := receivedMsgs[recipient]
+		if !exists {
+			t.Errorf("Failed to receive message from %s: %+v", &recipient, msg)
+		} else if !reflect.DeepEqual(msg, receivedMsg) {
+			t.Errorf("Received unexpected message for recipient %s."+
+				"\nexpected: %+v\nreceived: %+v", &recipient, msg, receivedMsg)
+		}
+	}
+}
+
+// Tests that Manager.newRandomMessages creates a non-empty map of messages and
+// that each message is unique.
+func TestManager_newRandomMessages(t *testing.T) {
+	m := newTestManager(10, 0, 0, false, t)
+	prng := NewPrng(42)
+
+	msgMap, err := m.newRandomMessages(prng)
+	if err != nil {
+		t.Errorf("newRandomMessages returned an error: %+v", err)
+	}
+
+	if len(msgMap) == 0 {
+		t.Error("Message map is empty.")
+	}
+
+	marshalledMsgs := make(map[string]format.Message, len(msgMap))
+	for _, msg := range msgMap {
+		msgString := base64.StdEncoding.EncodeToString(msg.Marshal())
+		if _, exists := marshalledMsgs[msgString]; exists {
+			t.Errorf("Message not unique.")
+		} else {
+			marshalledMsgs[msgString] = msg
+		}
+	}
+}
+
+// Tests that Manager.newRandomCmixMessage generates a cMix message with
+// populated contents, fingerprint, and MAC.
+func TestManager_newRandomCmixMessage(t *testing.T) {
+	m := newTestManager(0, 0, 0, false, t)
+	prng := NewPrng(42)
+
+	cMixMsg, err := m.newRandomCmixMessage(prng)
+	if err != nil {
+		t.Errorf("newRandomCmixMessage returned an error: %+v", err)
+	}
+
+	if bytes.Equal(cMixMsg.GetContents(), make([]byte, len(cMixMsg.GetContents()))) {
+		t.Error("cMix message contents not set.")
+	}
+
+	if cMixMsg.GetKeyFP() == (format.Fingerprint{}) {
+		t.Error("cMix message fingerprint not set.")
+	}
+
+	if bytes.Equal(cMixMsg.GetMac(), make([]byte, format.MacLen)) {
+		t.Error("cMix message MAC not set.")
+	}
+}
diff --git a/dummy/utils_test.go b/dummy/utils_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..7312effd4d1d7b9d138e467d764f08fd5ddbd401
--- /dev/null
+++ b/dummy/utils_test.go
@@ -0,0 +1,208 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                           //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+package dummy
+
+import (
+	"github.com/pkg/errors"
+	"gitlab.com/elixxir/client/interfaces"
+	"gitlab.com/elixxir/client/interfaces/message"
+	"gitlab.com/elixxir/client/interfaces/params"
+	"gitlab.com/elixxir/client/network/gateway"
+	"gitlab.com/elixxir/client/stoppable"
+	"gitlab.com/elixxir/client/storage"
+	"gitlab.com/elixxir/comms/network"
+	"gitlab.com/elixxir/crypto/e2e"
+	"gitlab.com/elixxir/crypto/fastRNG"
+	"gitlab.com/elixxir/primitives/format"
+	"gitlab.com/xx_network/comms/connect"
+	"gitlab.com/xx_network/crypto/csprng"
+	"gitlab.com/xx_network/primitives/id"
+	"gitlab.com/xx_network/primitives/id/ephemeral"
+	"gitlab.com/xx_network/primitives/ndf"
+	"io"
+	"math/rand"
+	"sync"
+	"testing"
+	"time"
+)
+
+////////////////////////////////////////////////////////////////////////////////
+// PRNG                                                                       //
+////////////////////////////////////////////////////////////////////////////////
+
+// Prng is a PRNG that satisfies the csprng.Source interface.
+type Prng struct{ prng io.Reader }
+
+func NewPrng(seed int64) csprng.Source     { return &Prng{rand.New(rand.NewSource(seed))} }
+func (s *Prng) Read(b []byte) (int, error) { return s.prng.Read(b) }
+func (s *Prng) SetSeed([]byte) error       { return nil }
+
+////////////////////////////////////////////////////////////////////////////////
+// Test Managers                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+// newTestManager creates a new Manager that has groups stored for testing. One
+// of the groups in the list is also returned.
+func newTestManager(maxNumMessages int, avgSendDelta, randomRange time.Duration,
+	sendErr bool, t *testing.T) *Manager {
+	m := &Manager{
+		maxNumMessages: maxNumMessages,
+		avgSendDelta:   avgSendDelta,
+		randomRange:    randomRange,
+		store:          storage.InitTestingSession(t),
+		net:            newTestNetworkManager(sendErr, t),
+		rng:            fastRNG.NewStreamGenerator(1000, 10, csprng.NewSystemRNG),
+	}
+
+	return m
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test Network Manager                                                       //
+////////////////////////////////////////////////////////////////////////////////
+
+// testNetworkManager is a test implementation of NetworkManager interface.
+type testNetworkManager struct {
+	instance *network.Instance
+	messages map[id.ID]format.Message
+	sendErr  bool
+	sync.RWMutex
+}
+
+func newTestNetworkManager(sendErr bool, t *testing.T) interfaces.NetworkManager {
+	instanceComms := &connect.ProtoComms{
+		Manager: connect.NewManagerTesting(t),
+	}
+
+	thisInstance, err := network.NewInstanceTesting(instanceComms, getNDF(),
+		getNDF(), nil, nil, t)
+	if err != nil {
+		t.Fatalf("Failed to create new test instance: %v", err)
+	}
+
+	return &testNetworkManager{
+		instance: thisInstance,
+		messages: make(map[id.ID]format.Message),
+		sendErr:  sendErr,
+	}
+}
+
+func (tnm *testNetworkManager) GetMsgListLen() int {
+	tnm.RLock()
+	defer tnm.RUnlock()
+	return len(tnm.messages)
+}
+
+func (tnm *testNetworkManager) GetMsgList() map[id.ID]format.Message {
+	tnm.RLock()
+	defer tnm.RUnlock()
+	return tnm.messages
+}
+
+func (tnm *testNetworkManager) GetMsg(recipient id.ID) format.Message {
+	tnm.RLock()
+	defer tnm.RUnlock()
+	return tnm.messages[recipient]
+}
+
+func (tnm *testNetworkManager) SendE2E(message.Send, params.E2E, *stoppable.Single) (
+	[]id.Round, e2e.MessageID, time.Time, error) {
+	return nil, e2e.MessageID{}, time.Time{}, nil
+}
+
+func (tnm *testNetworkManager) SendUnsafe(message.Send, params.Unsafe) ([]id.Round, error) {
+	return []id.Round{}, nil
+}
+
+func (tnm *testNetworkManager) SendCMIX(message format.Message,
+	recipient *id.ID, _ params.CMIX) (id.Round, ephemeral.Id, error) {
+	tnm.Lock()
+	defer tnm.Unlock()
+
+	if tnm.sendErr {
+		return 0, ephemeral.Id{}, errors.New("SendCMIX error")
+	}
+
+	tnm.messages[*recipient] = message
+
+	return 0, ephemeral.Id{}, nil
+}
+
+func (tnm *testNetworkManager) SendManyCMIX(map[id.ID]format.Message, params.CMIX) (
+	id.Round, []ephemeral.Id, error) {
+	return 0, nil, nil
+}
+
+type dummyEventMgr struct{}
+
+func (d *dummyEventMgr) Report(int, string, string, string) {}
+func (tnm *testNetworkManager) GetEventManager() interfaces.EventManager {
+	return &dummyEventMgr{}
+}
+
+func (tnm *testNetworkManager) GetInstance() *network.Instance             { return tnm.instance }
+func (tnm *testNetworkManager) GetHealthTracker() interfaces.HealthTracker { return nil }
+func (tnm *testNetworkManager) Follow(interfaces.ClientErrorReport) (stoppable.Stoppable, error) {
+	return nil, nil
+}
+func (tnm *testNetworkManager) CheckGarbledMessages()        {}
+func (tnm *testNetworkManager) InProgressRegistrations() int { return 0 }
+func (tnm *testNetworkManager) GetSender() *gateway.Sender   { return nil }
+func (tnm *testNetworkManager) GetAddressSize() uint8        { return 0 }
+func (tnm *testNetworkManager) RegisterAddressSizeNotification(string) (chan uint8, error) {
+	return nil, nil
+}
+func (tnm *testNetworkManager) UnregisterAddressSizeNotification(string) {}
+func (tnm *testNetworkManager) SetPoolFilter(gateway.Filter)             {}
+func (tnm *testNetworkManager) GetVerboseRounds() string                 { return "" }
+
+////////////////////////////////////////////////////////////////////////////////
+// NDF Primes                                                                 //
+////////////////////////////////////////////////////////////////////////////////
+
+func getNDF() *ndf.NetworkDefinition {
+	return &ndf.NetworkDefinition{
+		E2E: ndf.Group{
+			Prime: "E2EE983D031DC1DB6F1A7A67DF0E9A8E5561DB8E8D49413394C049B7A" +
+				"8ACCEDC298708F121951D9CF920EC5D146727AA4AE535B0922C688B55B3D" +
+				"D2AEDF6C01C94764DAB937935AA83BE36E67760713AB44A6337C20E78615" +
+				"75E745D31F8B9E9AD8412118C62A3E2E29DF46B0864D0C951C394A5CBBDC" +
+				"6ADC718DD2A3E041023DBB5AB23EBB4742DE9C1687B5B34FA48C3521632C" +
+				"4A530E8FFB1BC51DADDF453B0B2717C2BC6669ED76B4BDD5C9FF558E88F2" +
+				"6E5785302BEDBCA23EAC5ACE92096EE8A60642FB61E8F3D24990B8CB12EE" +
+				"448EEF78E184C7242DD161C7738F32BF29A841698978825B4111B4BC3E1E" +
+				"198455095958333D776D8B2BEEED3A1A1A221A6E37E664A64B83981C46FF" +
+				"DDC1A45E3D5211AAF8BFBC072768C4F50D7D7803D2D4F278DE8014A47323" +
+				"631D7E064DE81C0C6BFA43EF0E6998860F1390B5D3FEACAF1696015CB79C" +
+				"3F9C2D93D961120CD0E5F12CBB687EAB045241F96789C38E89D796138E63" +
+				"19BE62E35D87B1048CA28BE389B575E994DCA755471584A09EC723742DC3" +
+				"5873847AEF49F66E43873",
+			Generator: "2",
+		},
+		CMIX: ndf.Group{
+			Prime: "9DB6FB5951B66BB6FE1E140F1D2CE5502374161FD6538DF1648218642" +
+				"F0B5C48C8F7A41AADFA187324B87674FA1822B00F1ECF8136943D7C55757" +
+				"264E5A1A44FFE012E9936E00C1D3E9310B01C7D179805D3058B2A9F4BB6F" +
+				"9716BFE6117C6B5B3CC4D9BE341104AD4A80AD6C94E005F4B993E14F091E" +
+				"B51743BF33050C38DE235567E1B34C3D6A5C0CEAA1A0F368213C3D19843D" +
+				"0B4B09DCB9FC72D39C8DE41F1BF14D4BB4563CA28371621CAD3324B6A2D3" +
+				"92145BEBFAC748805236F5CA2FE92B871CD8F9C36D3292B5509CA8CAA77A" +
+				"2ADFC7BFD77DDA6F71125A7456FEA153E433256A2261C6A06ED3693797E7" +
+				"995FAD5AABBCFBE3EDA2741E375404AE25B",
+			Generator: "5C7FF6B06F8F143FE8288433493E4769C4D988ACE5BE25A0E2480" +
+				"9670716C613D7B0CEE6932F8FAA7C44D2CB24523DA53FBE4F6EC3595892D" +
+				"1AA58C4328A06C46A15662E7EAA703A1DECF8BBB2D05DBE2EB956C142A33" +
+				"8661D10461C0D135472085057F3494309FFA73C611F78B32ADBB5740C361" +
+				"C9F35BE90997DB2014E2EF5AA61782F52ABEB8BD6432C4DD097BC5423B28" +
+				"5DAFB60DC364E8161F4A2A35ACA3A10B1C4D203CC76A470A33AFDCBDD929" +
+				"59859ABD8B56E1725252D78EAC66E71BA9AE3F1DD2487199874393CD4D83" +
+				"2186800654760E1E34C09E4D155179F9EC0DC4473F996BDCE6EED1CABED8" +
+				"B6F116F7AD9CF505DF0F998E34AB27514B0FFE7",
+		},
+	}
+}