go-gomail / gomail

The best way to send emails in Go.
MIT License
4.3k stars 570 forks source link

How to mock the smtp server? #148

Closed gadelkareem closed 1 year ago

gadelkareem commented 4 years ago

Can you give me an example of how to mock the smtp server and verify the message after doing a request?

gadelkareem commented 4 years ago

The best I could do is

package tests

import (
    "bytes"
    "io"
    "regexp"
    "strings"
    "testing"

    "github.com/go-gomail/gomail"
    "github.com/stretchr/testify/assert"
)

type mockSender gomail.SendFunc

func (s mockSender) Send(from string, to []string, msg io.WriterTo) error {
    return s(from, to, msg)
}

type mockSendCloser struct {
    mockSender
    close func() error
}

func (s *mockSendCloser) Close() error {
    return s.close()
}

func newMockSenderCloser() *mockSendCloser {
    return &mockSendCloser{
        mockSender: func(from string, to []string, msg io.WriterTo) error { return nil },
        close:      func() error { return nil },
    }
}

func ExpectEmail(t *testing.T, wantFrom string, wantTo []string, wantSubject, wantBody string) {
    s := &mockSendCloser{
        mockSender: stubSend(t, wantFrom, wantTo, wantSubject, wantBody),
        close: func() error {
            t.Error("Close() should not be called in Send()")
            return nil
        },
    }
    // Container -> email service -> sendercloser
    C.EmailService.SenderCloser = s
}

var subjRegex = regexp.MustCompile(`(?m)^Subject: (.+)\r\n`)

func stubSend(t *testing.T, wantFrom string, wantTo []string, wantSubject, wantBody string) mockSender {
    return func(from string, to []string, msg io.WriterTo) error {

        assert.Equal(t, from, wantFrom)
        assert.ElementsMatch(t, to, wantTo)

        buf := new(bytes.Buffer)
        _, err := msg.WriteTo(buf)
        if err != nil {
            t.Fatal(err)
        }
        b := buf.String()

        if wantSubject != "" {
            subject := subjRegex.FindStringSubmatch(b)
            assert.Equal(t, wantSubject, subject[1])

        }
        if wantBody != "" {
            compareBodies(t, b, wantBody)
        }
        return nil
    }
}

func compareBodies(t *testing.T, got, want string) {
    // We cannot do a simple comparison since the ordering of headers' fields
    // is random.
    gotLines := strings.Split(got, "\r\n")
    wantLines := strings.Split(want, "\r\n")

    // We only test for too many lines, missing lines are tested after
    if len(gotLines) > len(wantLines) {
        t.Fatalf("Message has too many lines, \ngot %d:\n%s\nwant %d:\n%s", len(gotLines), got, len(wantLines), want)
    }

    isInHeader := true
    headerStart := 0
    for i, line := range wantLines {
        if line == gotLines[i] {
            if line == "" {
                isInHeader = false
            } else if !isInHeader && len(line) > 2 && line[:2] == "--" {
                isInHeader = true
                headerStart = i + 1
            }
            continue
        }

        if !isInHeader {
            missingLine(t, line, got, want)
        }

        isMissing := true
        for j := headerStart; j < len(gotLines); j++ {
            if gotLines[j] == "" {
                break
            }
            if gotLines[j] == line {
                isMissing = false
                break
            }
        }
        if isMissing {
            missingLine(t, line, got, want)
        }
    }
}

func missingLine(t *testing.T, line, got, want string) {
    t.Fatalf("Missing line %q\ngot:\n%s\nwant:\n%s", line, got, want)
}

There are 2 problems: 1- I have to send the EmailService both dialer and sendercloser, to able to use dialer in prod and sendcloser in tests

func NewEmailService(d *gomail.Dialer, sc gomail.SendCloser) *EmailService {
    return &EmailService{Dialer: d, SenderCloser: sc}
}

2- no way to run tests in parallel, but not sure if this is a Golang limitation since there is no testing context.

puffitos commented 1 year ago

Use github.com/mocktools/go-smtp-mock. Works like a charm.

gadelkareem commented 1 year ago

Thanks!