diff --git a/cmix/attempts/histrogram.go b/cmix/attempts/histrogram.go
index 0b6955dea3239edecd9f598191751edb9c315c63..37e7292592aeae1cdda482a82b0fdcf5530933d9 100644
--- a/cmix/attempts/histrogram.go
+++ b/cmix/attempts/histrogram.go
@@ -18,11 +18,19 @@ const (
 	optimalAttemptsInitValue    = -1
 )
 
+// SendAttemptTracker tracks the number of attempts it took to send a cMix
+// message in order to predict how many attempt are needed.
 type SendAttemptTracker interface {
+	// SubmitProbeAttempt feeds the number of attempts it took to send a cMix
+	// message into the tracker and updates the optimal number of attempts.
 	SubmitProbeAttempt(numAttemptsUntilSuccessful int)
+
+	// GetOptimalNumAttempts returns the number of optimal sends. If there is
+	// insufficient data to calculate, then ready is false.
 	GetOptimalNumAttempts() (attempts int, ready bool)
 }
 
+// sendAttempts tracks the number of attempts to send a cMix message.
 type sendAttempts struct {
 	optimalAttempts *int32
 	isFull          bool
@@ -31,6 +39,7 @@ type sendAttempts struct {
 	lock            sync.Mutex
 }
 
+// NewSendAttempts initialises a new SendAttemptTracker.
 func NewSendAttempts() SendAttemptTracker {
 	optimalAttempts := int32(optimalAttemptsInitValue)
 	sa := &sendAttempts{
@@ -43,6 +52,8 @@ func NewSendAttempts() SendAttemptTracker {
 	return sa
 }
 
+// SubmitProbeAttempt feeds the number of attempts it took to send a cMix
+// message into the tracker and updates the optimal number of attempts.
 func (sa *sendAttempts) SubmitProbeAttempt(numAttemptsUntilSuccessful int) {
 	sa.lock.Lock()
 	defer sa.lock.Unlock()
@@ -58,6 +69,8 @@ func (sa *sendAttempts) SubmitProbeAttempt(numAttemptsUntilSuccessful int) {
 	sa.computeOptimalUnsafe()
 }
 
+// GetOptimalNumAttempts returns the number of optimal sends. If there is
+// insufficient data to calculate, then ready is false.
 func (sa *sendAttempts) GetOptimalNumAttempts() (attempts int, ready bool) {
 	optimalAttempts := atomic.LoadInt32(sa.optimalAttempts)
 
@@ -68,6 +81,7 @@ func (sa *sendAttempts) GetOptimalNumAttempts() (attempts int, ready bool) {
 	return int(optimalAttempts), true
 }
 
+// computeOptimalUnsafe updates the optimal send attempts.
 func (sa *sendAttempts) computeOptimalUnsafe() {
 	toCopy := maxHistogramSize
 	if !sa.isFull {
diff --git a/cmix/attempts/histrogram_test.go b/cmix/attempts/histrogram_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..6934b1f332e44cd17dd5cf2030932a8ec364fbd3
--- /dev/null
+++ b/cmix/attempts/histrogram_test.go
@@ -0,0 +1,89 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+package attempts
+
+import (
+	"math/rand"
+	"reflect"
+	"testing"
+)
+
+// Tests that NewSendAttempts returns a new sendAttempts with the expected
+// fields.
+func TestNewSendAttempts(t *testing.T) {
+	optimalAttempts := int32(optimalAttemptsInitValue)
+	expected := &sendAttempts{
+		optimalAttempts: &optimalAttempts,
+		isFull:          false,
+		currentIndex:    0,
+		numAttempts:     make([]int, maxHistogramSize),
+	}
+
+	sa := NewSendAttempts()
+
+	if !reflect.DeepEqual(expected, sa) {
+		t.Errorf("New SendAttemptTracker does not match expected."+
+			"\nexpected: %+v\nreceivedL %+v", expected, sa)
+	}
+}
+
+// Tests that sendAttempts.SubmitProbeAttempt properly increments and stores the
+// attempts.
+func Test_sendAttempts_SubmitProbeAttempt(t *testing.T) {
+	sa := NewSendAttempts().(*sendAttempts)
+
+	for i := 0; i < maxHistogramSize+20; i++ {
+		sa.SubmitProbeAttempt(i)
+
+		if sa.currentIndex != (i+1)%maxHistogramSize {
+			t.Errorf("Incorrect currentIndex (%d).\nexpected: %d\nreceived: %d",
+				i, (i+1)%maxHistogramSize, sa.currentIndex)
+		} else if sa.numAttempts[i%maxHistogramSize] != i {
+			t.Errorf("Incorrect numAttempts at %d.\nexpected: %d\nreceived: %d",
+				i, i, sa.numAttempts[i%maxHistogramSize])
+		} else if i > maxHistogramSize && !sa.isFull {
+			t.Errorf("Should be marked full when numAttempts > %d.",
+				maxHistogramSize)
+		}
+	}
+}
+
+// Tests sendAttempts.GetOptimalNumAttempts returns numbers close to 70% of the
+// average of attempts feeding in.
+func Test_sendAttempts_GetOptimalNumAttempts(t *testing.T) {
+	prng := rand.New(rand.NewSource(42))
+	sa := NewSendAttempts().(*sendAttempts)
+
+	attempts, ready := sa.GetOptimalNumAttempts()
+	if ready {
+		t.Errorf("Marked ready when no attempts have been made.")
+	} else if attempts != 0 {
+		t.Errorf("Incorrect number of attempt.\nexpected: %d\nreceived: %d",
+			0, attempts)
+	}
+
+	const n = 100
+	factor := (n * 7) / 10
+	for i := 0; i < 500; i++ {
+		sa.SubmitProbeAttempt(prng.Intn(n))
+		attempts, ready = sa.GetOptimalNumAttempts()
+
+		if (sa.currentIndex < minElements && !sa.isFull) && ready {
+			t.Errorf("Ready when less than %d attempts made (%d).",
+				minElements, i)
+		} else if sa.currentIndex >= minElements {
+			if !ready {
+				t.Errorf("Not ready when more than %d attempts made (%d).",
+					minElements, i)
+			} else if attempts < factor-25 || attempts > factor+25 {
+				t.Errorf("Attempts is not close to average (%d)."+
+					"\naverage:  %d\nattempts: %d", i, factor, attempts)
+			}
+		}
+	}
+}