gRPC Server Testing with Node.js

February 14, 2024

Learn how to effectively test gRPC servers in Node.js applications. This comprehensive guide covers everything from basic setup to advanced testing patterns, including integration with Mail7 for email-related functionality testing.

Setting Up the Test Environment

1. Project Setup


// Initialize project
npm init -y

// Install dependencies
npm install @grpc/grpc-js @grpc/proto-loader
npm install --save-dev jest grpc-tools

2. Proto File Setup


// user_service.proto
syntax = "proto3";

package userservice;

service UserService {
    rpc CreateUser (CreateUserRequest) returns (CreateUserResponse);
    rpc VerifyEmail (VerifyEmailRequest) returns (VerifyEmailResponse);
    rpc UpdateUserProfile (UpdateUserRequest) returns (UpdateUserResponse);
}

message CreateUserRequest {
    string email = 1;
    string password = 2;
    string name = 3;
}

message CreateUserResponse {
    string user_id = 1;
    bool verification_email_sent = 2;
}

message VerifyEmailRequest {
    string verification_token = 1;
}

message VerifyEmailResponse {
    bool success = 1;
    string message = 2;
}

message UpdateUserRequest {
    string user_id = 1;
    optional string name = 2;
    optional string email = 3;
}

message UpdateUserResponse {
    bool success = 1;
    string message = 2;
}

Implementation Examples

1. Basic gRPC Server Setup


// server.js
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const Mail7 = require('@mail7/sdk');

const PROTO_PATH = './protos/user_service.proto';

const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
    keepCase: true,
    longs: String,
    enums: String,
    defaults: true,
    oneofs: true
});

const userProto = grpc.loadPackageDefinition(packageDefinition).userservice;

class UserService {
    constructor() {
        this.mail7 = new Mail7({
            apiKey: process.env.MAIL7_API_KEY,
            domain: process.env.MAIL7_DOMAIN
        });
    }

    async createUser(call, callback) {
        try {
            const { email, password, name } = call.request;
            
            // Create user in database
            const userId = await this.createUserInDb({ email, password, name });
            
            // Send verification email
            await this.mail7.sendEmail({
                to: email,
                subject: 'Verify Your Email',
                template: 'email-verification',
                variables: {
                    name,
                    verificationLink: this.generateVerificationLink(userId)
                }
            });

            callback(null, {
                user_id: userId,
                verification_email_sent: true
            });
        } catch (error) {
            callback({
                code: grpc.status.INTERNAL,
                details: error.message
            });
        }
    }

    async verifyEmail(call, callback) {
        try {
            const { verification_token } = call.request;
            const result = await this.verifyEmailToken(verification_token);
            
            callback(null, {
                success: result.success,
                message: result.message
            });
        } catch (error) {
            callback({
                code: grpc.status.INTERNAL,
                details: error.message
            });
        }
    }
}

2. Test Server Setup


// test_server.js
class TestServer {
    constructor() {
        this.server = new grpc.Server();
        this.userService = new UserService();
        
        this.server.addService(
            userProto.UserService.service,
            {
                createUser: this.userService.createUser.bind(this.userService),
                verifyEmail: this.userService.verifyEmail.bind(this.userService),
                updateUserProfile: this.userService.updateUserProfile.bind(this.userService)
            }
        );
    }

    async start() {
        return new Promise((resolve, reject) => {
            this.server.bindAsync(
                '127.0.0.1:0',
                grpc.ServerCredentials.createInsecure(),
                (error, port) => {
                    if (error) {
                        reject(error);
                        return;
                    }
                    this.port = port;
                    this.server.start();
                    resolve(port);
                }
            );
        });
    }

    async stop() {
        return new Promise((resolve) => {
            this.server.tryShutdown(() => {
                resolve();
            });
        });
    }

    getClient() {
        return new userProto.UserService(
            `localhost:${this.port}`,
            grpc.credentials.createInsecure()
        );
    }
}

3. Integration Tests


// user_service.test.js
describe('UserService', () => {
    let server;
    let client;
    let testEmail;

    beforeAll(async () => {
        // Start test server
        server = new TestServer();
        await server.start();
        client = server.getClient();

        // Generate test email
        const mail7 = new Mail7(config);
        testEmail = await mail7.generateEmail();
    });

    afterAll(async () => {
        await server.stop();
    });

    describe('createUser', () => {
        it('should create user and send verification email', async () => {
            const response = await new Promise((resolve, reject) => {
                client.createUser({
                    email: testEmail,
                    password: 'testpass123',
                    name: 'Test User'
                }, (error, response) => {
                    if (error) reject(error);
                    else resolve(response);
                });
            });

            expect(response.user_id).toBeDefined();
            expect(response.verification_email_sent).toBe(true);

            // Verify email was sent
            const email = await mail7.waitForEmail({
                to: testEmail,
                subject: 'Verify Your Email',
                timeout: 5000
            });

            expect(email).toBeDefined();
            expect(email.html).toContain('Test User');
            expect(email.html).toContain('verification');
        });

        it('should handle invalid email format', async () => {
            try {
                await new Promise((resolve, reject) => {
                    client.createUser({
                        email: 'invalid-email',
                        password: 'testpass123',
                        name: 'Test User'
                    }, (error, response) => {
                        if (error) reject(error);
                        else resolve(response);
                    });
                });
                fail('Should have thrown an error');
            } catch (error) {
                expect(error.code).toBe(grpc.status.INVALID_ARGUMENT);
                expect(error.details).toContain('Invalid email format');
            }
        });
    });

    describe('verifyEmail', () => {
        it('should verify valid token', async () => {
            const response = await new Promise((resolve, reject) => {
                client.verifyEmail({
                    verification_token: 'valid-token'
                }, (error, response) => {
                    if (error) reject(error);
                    else resolve(response);
                });
            });

            expect(response.success).toBe(true);
            expect(response.message).toContain('verified');
        });
    });
});

Advanced Testing Patterns

1. Mock Services


class MockUserService {
    constructor() {
        this.users = new Map();
        this.verificationTokens = new Map();
    }

    async createUser(call, callback) {
        const { email, password, name } = call.request;
        const userId = `user_${Date.now()}`;
        
        this.users.set(userId, { email, name, verified: false });
        const token = this.generateToken(userId);
        this.verificationTokens.set(token, userId);

        callback(null, {
            user_id: userId,
            verification_email_sent: true
        });
    }

    async verifyEmail(call, callback) {
        const { verification_token } = call.request;
        const userId = this.verificationTokens.get(verification_token);
        
        if (!userId) {
            callback({
                code: grpc.status.NOT_FOUND,
                details: 'Invalid token'
            });
            return;
        }

        const user = this.users.get(userId);
        user.verified = true;
        this.users.set(userId, user);

        callback(null, {
            success: true,
            message: 'Email verified successfully'
        });
    }
}

2. Load Testing


const grpc_load = require('grpc-load');

async function runLoadTest() {
    const test = new grpc_load.LoadTest({
        server: 'localhost:50051',
        service: userProto.UserService,
        testDuration: '30s',
        requestsPerSecond: 100
    });

    test.addTestCase('createUser', {
        request: () => ({
            email: `test${Date.now()}@mail7.io`,
            password: 'testpass123',
            name: 'Test User'
        }),
        validator: (response) => {
            return response.user_id && response.verification_email_sent;
        }
    });

    const results = await test.run();
    console.log('Load Test Results:', results);
}

3. Error Injection


class ErrorInjectionProxy {
    constructor(target) {
        this.target = target;
        this.errorScenarios = new Map();
    }

    addErrorScenario(method, condition, error) {
        if (!this.errorScenarios.has(method)) {
            this.errorScenarios.set(method, []);
        }
        this.errorScenarios.get(method).push({ condition, error });
    }

    async handleRequest(method, call, callback) {
        const scenarios = this.errorScenarios.get(method) || [];
        
        for (const scenario of scenarios) {
            if (scenario.condition(call.request)) {
                callback(scenario.error);
                return;
            }
        }

        return this.target[method](call, callback);
    }
}

Best Practices

1. Test Organization

  • Group tests by service and method
  • Use descriptive test names
  • Implement proper test lifecycle hooks
  • Isolate test data between runs

2. Error Handling

  • Test all error scenarios
  • Verify error codes and messages
  • Test timeout scenarios
  • Implement proper cleanup in error cases

3. Performance Testing

  • Monitor response times
  • Test under different loads
  • Verify resource cleanup
  • Test connection handling

4. Security Testing

  • Test authentication and authorization
  • Verify SSL/TLS configuration
  • Test input validation
  • Check for proper error masking

Troubleshooting

Connection Issues

  • Verify server is running
  • Check port availability
  • Verify credentials
  • Check network connectivity

Test Failures

  • Check test environment
  • Verify test data
  • Review error logs
  • Check for race conditions

Performance Issues

  • Monitor resource usage
  • Check for memory leaks
  • Optimize test setup
  • Review connection pooling

Start Testing Now

Ready to implement robust gRPC server testing in your Node.js application? Get started with our comprehensive toolkit: