go-micro.dev/v5@v5.12.0/test/service.go (about)

     1  // Package test implements a testing framwork, and provides default tests.
     2  package test
     3  
     4  import (
     5  	"context"
     6  	"fmt"
     7  	"sync"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/pkg/errors"
    12  
    13  	"go-micro.dev/v5"
    14  	"go-micro.dev/v5/client"
    15  	"go-micro.dev/v5/debug/handler"
    16  
    17  	pb "go-micro.dev/v5/debug/proto"
    18  )
    19  
    20  var (
    21  	// ErrNoTests returns no test params are set.
    22  	ErrNoTests = errors.New("No tests to run, all values set to 0")
    23  	testTopic  = "Test-Topic"
    24  	errorTopic = "Error-Topic"
    25  )
    26  
    27  type parTest func(name string, c client.Client, p, s int, errChan chan error)
    28  type testFunc func(name string, c client.Client, errChan chan error)
    29  
    30  // ServiceTestConfig allows you to easily test a service configuration by
    31  // running predefined tests against your custom service. You only need to
    32  // provide a function to create the service, and how many of which test you
    33  // want to run.
    34  //
    35  // The default tests provided, all running with separate parallel routines are:
    36  //   - Sequential Call requests
    37  //   - Bi-directional streaming
    38  //   - Pub/Sub events brokering
    39  //
    40  // You can provide an array of parallel routines to run for the request and
    41  // stream tests. They will be run as matrix tests, so with each possible combination.
    42  // Thus, in total (p * seq) + (p * streams) tests will be run.
    43  type ServiceTestConfig struct {
    44  	// Service name to use for the tests
    45  	Name string
    46  	// NewService function will be called to setup the new service.
    47  	// It takes in a list of options, which by default will Context and an
    48  	// AfterStart with channel to signal when the service has been started.
    49  	NewService func(name string, opts ...micro.Option) (micro.Service, error)
    50  	// Parallel is the number of prallell routines to use for the tests.
    51  	Parallel []int
    52  	// Sequential is the number of sequential requests to send per parallel process.
    53  	Sequential []int
    54  	// Streams is the nummber of streaming messages to send over the stream per routine.
    55  	Streams []int
    56  	// PubSub is the number of times to publish messages to the broker per routine.
    57  	PubSub []int
    58  
    59  	mu       sync.Mutex
    60  	msgCount int
    61  }
    62  
    63  // Run will start the benchmark tests.
    64  func (stc *ServiceTestConfig) Run(b *testing.B) {
    65  	if err := stc.validate(); err != nil {
    66  		b.Fatal("Failed to validate config", err)
    67  	}
    68  
    69  	// Run routines with sequential requests
    70  	stc.prepBench(b, "req", stc.runParSeqTest, stc.Sequential)
    71  
    72  	// Run routines with streams
    73  	stc.prepBench(b, "streams", stc.runParStreamTest, stc.Streams)
    74  
    75  	// Run routines with pub/sub
    76  	stc.prepBench(b, "pubsub", stc.runBrokerTest, stc.PubSub)
    77  }
    78  
    79  // prepBench will prepare the benmark by setting the right parameters,
    80  // and invoking the test.
    81  func (stc *ServiceTestConfig) prepBench(b *testing.B, tName string, test parTest, seq []int) {
    82  	par := stc.Parallel
    83  
    84  	// No requests needed
    85  	if len(seq) == 0 || seq[0] == 0 {
    86  		return
    87  	}
    88  
    89  	for _, parallel := range par {
    90  		for _, sequential := range seq {
    91  			// Create the service name for the test
    92  			name := fmt.Sprintf("%s.%dp-%d%s", stc.Name, parallel, sequential, tName)
    93  
    94  			// Run test with parallel routines making each sequential requests
    95  			test := func(name string, c client.Client, errChan chan error) {
    96  				test(name, c, parallel, sequential, errChan)
    97  			}
    98  
    99  			benchmark := func(b *testing.B) {
   100  				b.ReportAllocs()
   101  				stc.runBench(b, name, test)
   102  			}
   103  
   104  			b.Logf("----------- STARTING TEST %s -----------", name)
   105  
   106  			// Run test, return if it fails
   107  			if !b.Run(name, benchmark) {
   108  				return
   109  			}
   110  		}
   111  	}
   112  }
   113  
   114  // runParSeqTest will make s sequential requests in p parallel routines.
   115  func (stc *ServiceTestConfig) runParSeqTest(name string, c client.Client, p, s int, errChan chan error) {
   116  	testParallel(p, func() {
   117  		// Make serial requests
   118  		for z := 0; z < s; z++ {
   119  			if err := testRequest(context.Background(), c, name); err != nil {
   120  				errChan <- errors.Wrapf(err, "[%s] Request failed during testRequest", name)
   121  				return
   122  			}
   123  		}
   124  	})
   125  }
   126  
   127  // Handle is used as a test handler.
   128  func (stc *ServiceTestConfig) Handle(ctx context.Context, msg *pb.HealthRequest) error {
   129  	stc.mu.Lock()
   130  	stc.msgCount++
   131  	stc.mu.Unlock()
   132  
   133  	return nil
   134  }
   135  
   136  // HandleError is used as a test handler.
   137  func (stc *ServiceTestConfig) HandleError(ctx context.Context, msg *pb.HealthRequest) error {
   138  	return errors.New("dummy error")
   139  }
   140  
   141  // runBrokerTest will publish messages to the broker to test pub/sub.
   142  func (stc *ServiceTestConfig) runBrokerTest(name string, c client.Client, p, s int, errChan chan error) {
   143  	stc.msgCount = 0
   144  
   145  	testParallel(p, func() {
   146  		for z := 0; z < s; z++ {
   147  			msg := pb.BusMsg{Msg: "Hello from broker!"}
   148  			if err := c.Publish(context.Background(), c.NewMessage(testTopic, &msg)); err != nil {
   149  				errChan <- errors.Wrap(err, "failed to publish message to broker")
   150  				return
   151  			}
   152  
   153  			msg = pb.BusMsg{Msg: "Some message that will error"}
   154  			if err := c.Publish(context.Background(), c.NewMessage(errorTopic, &msg)); err == nil {
   155  				errChan <- errors.New("Publish is supposed to return an error, but got no error")
   156  				return
   157  			}
   158  		}
   159  	})
   160  
   161  	if stc.msgCount != s*p {
   162  		errChan <- fmt.Errorf("pub/sub does not work properly, invalid message count. Expected %d messaged, but received %d", s*p, stc.msgCount)
   163  		return
   164  	}
   165  }
   166  
   167  // runParStreamTest will start streaming, and send s messages parallel in p routines.
   168  func (stc *ServiceTestConfig) runParStreamTest(name string, c client.Client, p, s int, errChan chan error) {
   169  	testParallel(p, func() {
   170  		// Create a client service
   171  		srv := pb.NewDebugService(name, c)
   172  
   173  		// Establish a connection to server over which we start streaming
   174  		bus, err := srv.MessageBus(context.Background())
   175  		if err != nil {
   176  			errChan <- errors.Wrap(err, "failed to connect to message bus")
   177  			return
   178  		}
   179  
   180  		// Start streaming requests
   181  		for z := 0; z < s; z++ {
   182  			if err := bus.Send(&pb.BusMsg{Msg: "Hack the world!"}); err != nil {
   183  				errChan <- errors.Wrap(err, "failed to send to  stream")
   184  				return
   185  			}
   186  
   187  			msg, err := bus.Recv()
   188  			if err != nil {
   189  				errChan <- errors.Wrap(err, "failed to receive message from stream")
   190  				return
   191  			}
   192  
   193  			expected := "Request received!"
   194  			if msg.Msg != expected {
   195  				errChan <- fmt.Errorf("stream returned unexpected mesage. Expected '%s', but got '%s'", expected, msg.Msg)
   196  				return
   197  			}
   198  		}
   199  	})
   200  }
   201  
   202  // validate will make sure the provided test parameters are a legal combination.
   203  func (stc *ServiceTestConfig) validate() error {
   204  	lp, lseq, lstr := len(stc.Parallel), len(stc.Sequential), len(stc.Streams)
   205  
   206  	if lp == 0 || (lseq == 0 && lstr == 0) {
   207  		return ErrNoTests
   208  	}
   209  
   210  	return nil
   211  }
   212  
   213  // runBench will create a service with the provided stc.NewService function,
   214  // and run a benchmark on the test function.
   215  func (stc *ServiceTestConfig) runBench(b *testing.B, name string, test testFunc) {
   216  	b.StopTimer()
   217  
   218  	// Channel to signal service has started
   219  	started := make(chan struct{})
   220  
   221  	// Context with cancel to stop the service
   222  	ctx, cancel := context.WithCancel(context.Background())
   223  
   224  	opts := []micro.Option{
   225  		micro.Context(ctx),
   226  		micro.AfterStart(func() error {
   227  			started <- struct{}{}
   228  			return nil
   229  		}),
   230  	}
   231  
   232  	// Create a new service per test
   233  	service, err := stc.NewService(name, opts...)
   234  	if err != nil {
   235  		b.Fatalf("failed to create service: %v", err)
   236  	}
   237  
   238  	// Register handler
   239  	if err := pb.RegisterDebugHandler(service.Server(), handler.NewHandler(service.Client())); err != nil {
   240  		b.Fatalf("failed to register handler during initial service setup: %v", err)
   241  	}
   242  
   243  	o := service.Options()
   244  	if err := o.Broker.Connect(); err != nil {
   245  		b.Fatal(err)
   246  	}
   247  
   248  	// a := new(testService)
   249  	if err := o.Server.Subscribe(o.Server.NewSubscriber(testTopic, stc.Handle)); err != nil {
   250  		b.Fatalf("[%s] Failed to register subscriber: %v", name, err)
   251  	}
   252  
   253  	if err := o.Server.Subscribe(o.Server.NewSubscriber(errorTopic, stc.HandleError)); err != nil {
   254  		b.Fatalf("[%s] Failed to register subscriber: %v", name, err)
   255  	}
   256  
   257  	b.Logf("# == [ Service ] ==================")
   258  	b.Logf("#    * Server: %s", o.Server.String())
   259  	b.Logf("#    * Client: %s", o.Client.String())
   260  	b.Logf("#    * Transport: %s", o.Transport.String())
   261  	b.Logf("#    * Broker: %s", o.Broker.String())
   262  	b.Logf("#    * Registry: %s", o.Registry.String())
   263  	b.Logf("#    * Auth: %s", o.Auth.String())
   264  	b.Logf("#    * Cache: %s", o.Cache.String())
   265  	b.Logf("# ================================")
   266  
   267  	RunBenchmark(b, name, service, test, cancel, started)
   268  }
   269  
   270  // RunBenchmark will run benchmarks on a provided service.
   271  //
   272  // A test function can be provided that will be fun b.N times.
   273  func RunBenchmark(b *testing.B, name string, service micro.Service, test testFunc,
   274  	cancel context.CancelFunc, started chan struct{}) {
   275  	b.StopTimer()
   276  
   277  	// Receive errors from routines on this channel
   278  	errChan := make(chan error, 1)
   279  
   280  	// Receive singal after service has shutdown
   281  	done := make(chan struct{})
   282  
   283  	// Start the server
   284  	go func() {
   285  		b.Logf("[%s] Starting server for benchmark", name)
   286  
   287  		if err := service.Run(); err != nil {
   288  			errChan <- errors.Wrapf(err, "[%s] Error occurred during service.Run", name)
   289  		}
   290  		done <- struct{}{}
   291  	}()
   292  
   293  	sigTerm := make(chan struct{})
   294  
   295  	// Benchmark routine
   296  	go func() {
   297  		defer func() {
   298  			b.StopTimer()
   299  
   300  			// Shutdown service
   301  			b.Logf("[%s] Shutting down", name)
   302  			cancel()
   303  
   304  			// Wait for service to be fully stopped
   305  			<-done
   306  			sigTerm <- struct{}{}
   307  		}()
   308  
   309  		// Wait for service to start
   310  		<-started
   311  
   312  		// Give the registry more time to setup
   313  		time.Sleep(time.Second)
   314  
   315  		b.Logf("[%s] Server started", name)
   316  
   317  		// Make a test call to warm the cache
   318  		for i := 0; i < 10; i++ {
   319  			if err := testRequest(context.Background(), service.Client(), name); err != nil {
   320  				errChan <- errors.Wrapf(err, "[%s] Failure during cache warmup testRequest", name)
   321  			}
   322  		}
   323  
   324  		// Check registration
   325  		services, err := service.Options().Registry.GetService(name)
   326  		if err != nil || len(services) == 0 {
   327  			errChan <- fmt.Errorf("service registration must have failed (%d services found), unable to get service: %w", len(services), err)
   328  			return
   329  		}
   330  
   331  		// Start benchmark
   332  		b.Logf("[%s] Starting benchtest", name)
   333  		b.ResetTimer()
   334  		b.StartTimer()
   335  
   336  		// Number of iterations
   337  		for i := 0; i < b.N; i++ {
   338  			test(name, service.Client(), errChan)
   339  		}
   340  	}()
   341  
   342  	// Wait for completion or catch any errors
   343  	select {
   344  	case err := <-errChan:
   345  		b.Fatal(err)
   346  	case <-sigTerm:
   347  		b.Logf("[%s] Completed benchmark", name)
   348  	}
   349  }
   350  
   351  // testParallel will run the test function in p parallel routines.
   352  func testParallel(p int, test func()) {
   353  	// Waitgroup to wait for requests to finish
   354  	wg := sync.WaitGroup{}
   355  
   356  	// For concurrency
   357  	for j := 0; j < p; j++ {
   358  		wg.Add(1)
   359  
   360  		go func() {
   361  			defer wg.Done()
   362  
   363  			test()
   364  		}()
   365  	}
   366  
   367  	// Wait for test completion
   368  	wg.Wait()
   369  }
   370  
   371  // testRequest sends one test request.
   372  // It calls the Debug.Health endpoint, and validates if the response returned
   373  // contains the expected message.
   374  func testRequest(ctx context.Context, c client.Client, name string) error {
   375  	req := c.NewRequest(
   376  		name,
   377  		"Debug.Health",
   378  		new(pb.HealthRequest),
   379  	)
   380  
   381  	rsp := new(pb.HealthResponse)
   382  
   383  	if err := c.Call(ctx, req, rsp); err != nil {
   384  		return err
   385  	}
   386  
   387  	if rsp.Status != "ok" {
   388  		return errors.New("service response: " + rsp.Status)
   389  	}
   390  
   391  	return nil
   392  }