- 统一文件命名为snake_case格式(kebab-case snake_case) - 重构zulip模块为zulip_core,明确Core层职责 - 重构user-mgmt模块为user_mgmt,统一命名规范 - 调整模块依赖关系,优化架构分层 - 删除过时的文件和目录结构 - 更新相关文档和配置文件 本次重构涉及大量文件重命名和模块重组, 旨在建立更清晰的项目架构和统一的命名规范。
553 lines
16 KiB
TypeScript
553 lines
16 KiB
TypeScript
/**
|
||
* RealRedisService集成测试
|
||
*
|
||
* 功能描述:
|
||
* - 使用真实Redis连接进行集成测试
|
||
* - 测试Redis服务器连接和断开
|
||
* - 验证数据持久性和一致性
|
||
* - 测试Redis服务的完整工作流程
|
||
*
|
||
* 职责分离:
|
||
* - 集成测试:测试与真实Redis服务器的交互
|
||
* - 连接测试:验证Redis连接管理
|
||
* - 数据一致性:测试数据的持久化和读取
|
||
* - 性能测试:验证Redis操作的性能表现
|
||
*
|
||
* 最近修改:
|
||
* - 2025-01-07: 功能新增 - 创建RealRedisService完整集成测试,验证真实Redis交互
|
||
*
|
||
* @author moyin
|
||
* @version 1.0.0
|
||
* @since 2025-01-07
|
||
* @lastModified 2025-01-07
|
||
*/
|
||
import { Test, TestingModule } from '@nestjs/testing';
|
||
import { ConfigService } from '@nestjs/config';
|
||
import { RealRedisService } from './real_redis.service';
|
||
import Redis from 'ioredis';
|
||
|
||
describe('RealRedisService Integration', () => {
|
||
let service: RealRedisService;
|
||
let module: TestingModule;
|
||
let configService: ConfigService;
|
||
|
||
// 测试配置 - 使用测试Redis实例
|
||
const testRedisConfig = {
|
||
REDIS_HOST: process.env.TEST_REDIS_HOST || 'localhost',
|
||
REDIS_PORT: parseInt(process.env.TEST_REDIS_PORT || '6379'),
|
||
REDIS_PASSWORD: process.env.TEST_REDIS_PASSWORD,
|
||
REDIS_DB: parseInt(process.env.TEST_REDIS_DB || '15'), // 使用DB 15进行测试
|
||
};
|
||
|
||
beforeAll(async () => {
|
||
// 检查是否有可用的Redis服务器
|
||
const testRedis = new Redis({
|
||
host: testRedisConfig.REDIS_HOST,
|
||
port: testRedisConfig.REDIS_PORT,
|
||
password: testRedisConfig.REDIS_PASSWORD,
|
||
db: testRedisConfig.REDIS_DB,
|
||
lazyConnect: true,
|
||
maxRetriesPerRequest: 1,
|
||
});
|
||
|
||
try {
|
||
await testRedis.ping();
|
||
// 确保连接被正确断开
|
||
testRedis.disconnect(false);
|
||
} catch (error) {
|
||
console.warn('Redis服务器不可用,跳过集成测试:', (error as Error).message);
|
||
// 确保连接被正确断开
|
||
testRedis.disconnect(false);
|
||
return;
|
||
}
|
||
|
||
// 创建测试模块
|
||
module = await Test.createTestingModule({
|
||
providers: [
|
||
RealRedisService,
|
||
{
|
||
provide: ConfigService,
|
||
useValue: {
|
||
get: jest.fn((key: string, defaultValue?: any) => {
|
||
return testRedisConfig[key] || defaultValue;
|
||
}),
|
||
},
|
||
},
|
||
],
|
||
}).compile();
|
||
|
||
service = module.get<RealRedisService>(RealRedisService);
|
||
configService = module.get<ConfigService>(ConfigService);
|
||
|
||
// 清空测试数据库
|
||
try {
|
||
await service.flushall();
|
||
} catch (error) {
|
||
// 忽略清空数据时的错误
|
||
}
|
||
});
|
||
|
||
afterAll(async () => {
|
||
if (service) {
|
||
try {
|
||
// 清空测试数据
|
||
await service.flushall();
|
||
} catch (error) {
|
||
// 忽略清空数据时的错误
|
||
}
|
||
|
||
try {
|
||
// 断开连接
|
||
service.onModuleDestroy();
|
||
} catch (error) {
|
||
// 忽略断开连接时的错误
|
||
}
|
||
}
|
||
if (module) {
|
||
await module.close();
|
||
}
|
||
});
|
||
|
||
beforeEach(async () => {
|
||
// 每个测试前清空数据
|
||
if (service) {
|
||
try {
|
||
await service.flushall();
|
||
} catch (error) {
|
||
// 忽略清空数据时的错误
|
||
}
|
||
}
|
||
});
|
||
|
||
// 检查Redis是否可用的辅助函数
|
||
const skipIfRedisUnavailable = () => {
|
||
if (!service) {
|
||
return true; // 返回true表示应该跳过测试
|
||
}
|
||
return false;
|
||
};
|
||
|
||
describe('Redis连接管理', () => {
|
||
it('should connect to Redis server successfully', () => {
|
||
if (skipIfRedisUnavailable()) {
|
||
console.log('跳过测试:Redis服务器不可用');
|
||
return;
|
||
}
|
||
expect(service).toBeDefined();
|
||
});
|
||
|
||
it('should use correct Redis configuration', () => {
|
||
if (skipIfRedisUnavailable()) {
|
||
console.log('跳过测试:Redis服务器不可用');
|
||
return;
|
||
}
|
||
expect(configService.get).toHaveBeenCalledWith('REDIS_HOST', 'localhost');
|
||
expect(configService.get).toHaveBeenCalledWith('REDIS_PORT', 6379);
|
||
expect(configService.get).toHaveBeenCalledWith('REDIS_DB', 0);
|
||
});
|
||
});
|
||
|
||
describe('基础键值操作', () => {
|
||
it('should set and get string values', async () => {
|
||
if (skipIfRedisUnavailable()) {
|
||
console.log('跳过测试:Redis服务器不可用');
|
||
return;
|
||
}
|
||
|
||
await service.set('test:string', 'Hello Redis');
|
||
const result = await service.get('test:string');
|
||
|
||
expect(result).toBe('Hello Redis');
|
||
});
|
||
|
||
it('should handle non-existent keys', async () => {
|
||
if (skipIfRedisUnavailable()) {
|
||
console.log('跳过测试:Redis服务器不可用');
|
||
return;
|
||
}
|
||
|
||
const result = await service.get('test:nonexistent');
|
||
|
||
expect(result).toBeNull();
|
||
});
|
||
|
||
it('should delete existing keys', async () => {
|
||
if (skipIfRedisUnavailable()) {
|
||
console.log('跳过测试:Redis服务器不可用');
|
||
return;
|
||
}
|
||
|
||
await service.set('test:delete', 'to be deleted');
|
||
const deleted = await service.del('test:delete');
|
||
const result = await service.get('test:delete');
|
||
|
||
expect(deleted).toBe(true);
|
||
expect(result).toBeNull();
|
||
});
|
||
|
||
it('should return false when deleting non-existent keys', async () => {
|
||
if (skipIfRedisUnavailable()) {
|
||
console.log('跳过测试:Redis服务器不可用');
|
||
return;
|
||
}
|
||
|
||
const deleted = await service.del('test:nonexistent');
|
||
|
||
expect(deleted).toBe(false);
|
||
});
|
||
|
||
it('should check key existence', async () => {
|
||
if (skipIfRedisUnavailable()) {
|
||
console.log('跳过测试:Redis服务器不可用');
|
||
return;
|
||
}
|
||
|
||
await service.set('test:exists', 'exists');
|
||
const exists = await service.exists('test:exists');
|
||
const notExists = await service.exists('test:notexists');
|
||
|
||
expect(exists).toBe(true);
|
||
expect(notExists).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('过期时间管理', () => {
|
||
it('should set keys with TTL', async () => {
|
||
if (skipIfRedisUnavailable()) {
|
||
console.log('跳过测试:Redis服务器不可用');
|
||
return;
|
||
}
|
||
|
||
await service.set('test:ttl', 'expires soon', 2);
|
||
const ttl = await service.ttl('test:ttl');
|
||
|
||
expect(ttl).toBeGreaterThan(0);
|
||
expect(ttl).toBeLessThanOrEqual(2);
|
||
});
|
||
|
||
it('should expire keys after TTL', async () => {
|
||
if (skipIfRedisUnavailable()) {
|
||
console.log('跳过测试:Redis服务器不可用');
|
||
return;
|
||
}
|
||
|
||
await service.set('test:expire', 'will expire', 1);
|
||
|
||
// 等待过期
|
||
await new Promise(resolve => setTimeout(resolve, 1100));
|
||
|
||
const result = await service.get('test:expire');
|
||
expect(result).toBeNull();
|
||
}, 2000);
|
||
|
||
it('should set expiration on existing keys', async () => {
|
||
if (skipIfRedisUnavailable()) {
|
||
console.log('跳过测试:Redis服务器不可用');
|
||
return;
|
||
}
|
||
|
||
await service.set('test:expire_later', 'set expiration later');
|
||
await service.expire('test:expire_later', 2);
|
||
|
||
const ttl = await service.ttl('test:expire_later');
|
||
expect(ttl).toBeGreaterThan(0);
|
||
expect(ttl).toBeLessThanOrEqual(2);
|
||
});
|
||
|
||
it('should return -1 for keys without expiration', async () => {
|
||
if (skipIfRedisUnavailable()) {
|
||
console.log('跳过测试:Redis服务器不可用');
|
||
return;
|
||
}
|
||
|
||
await service.set('test:no_expire', 'never expires');
|
||
const ttl = await service.ttl('test:no_expire');
|
||
|
||
expect(ttl).toBe(-1);
|
||
});
|
||
|
||
it('should return -2 for non-existent keys', async () => {
|
||
if (skipIfRedisUnavailable()) {
|
||
console.log('跳过测试:Redis服务器不可用');
|
||
return;
|
||
}
|
||
|
||
const ttl = await service.ttl('test:nonexistent');
|
||
|
||
expect(ttl).toBe(-2);
|
||
});
|
||
|
||
it('should use setex for atomic set with expiration', async () => {
|
||
if (skipIfRedisUnavailable()) {
|
||
console.log('跳过测试:Redis服务器不可用');
|
||
return;
|
||
}
|
||
|
||
await service.setex('test:setex', 2, 'atomic set with expiration');
|
||
const value = await service.get('test:setex');
|
||
const ttl = await service.ttl('test:setex');
|
||
|
||
expect(value).toBe('atomic set with expiration');
|
||
expect(ttl).toBeGreaterThan(0);
|
||
expect(ttl).toBeLessThanOrEqual(2);
|
||
});
|
||
});
|
||
|
||
describe('数值操作', () => {
|
||
it('should increment numeric values', async () => {
|
||
if (skipIfRedisUnavailable()) {
|
||
console.log('跳过测试:Redis服务器不可用');
|
||
return;
|
||
}
|
||
|
||
const result1 = await service.incr('test:counter');
|
||
const result2 = await service.incr('test:counter');
|
||
const result3 = await service.incr('test:counter');
|
||
|
||
expect(result1).toBe(1);
|
||
expect(result2).toBe(2);
|
||
expect(result3).toBe(3);
|
||
});
|
||
|
||
it('should increment existing numeric values', async () => {
|
||
if (skipIfRedisUnavailable()) {
|
||
console.log('跳过测试:Redis服务器不可用');
|
||
return;
|
||
}
|
||
|
||
await service.set('test:existing_counter', '10');
|
||
const result = await service.incr('test:existing_counter');
|
||
|
||
expect(result).toBe(11);
|
||
});
|
||
});
|
||
|
||
describe('集合操作', () => {
|
||
it('should add and retrieve set members', async () => {
|
||
if (skipIfRedisUnavailable()) {
|
||
console.log('跳过测试:Redis服务器不可用');
|
||
return;
|
||
}
|
||
|
||
await service.sadd('test:set', 'member1');
|
||
await service.sadd('test:set', 'member2');
|
||
await service.sadd('test:set', 'member3');
|
||
|
||
const members = await service.smembers('test:set');
|
||
|
||
expect(members).toHaveLength(3);
|
||
expect(members).toContain('member1');
|
||
expect(members).toContain('member2');
|
||
expect(members).toContain('member3');
|
||
});
|
||
|
||
it('should remove set members', async () => {
|
||
if (skipIfRedisUnavailable()) {
|
||
console.log('跳过测试:Redis服务器不可用');
|
||
return;
|
||
}
|
||
|
||
await service.sadd('test:set_remove', 'member1');
|
||
await service.sadd('test:set_remove', 'member2');
|
||
await service.sadd('test:set_remove', 'member3');
|
||
|
||
await service.srem('test:set_remove', 'member2');
|
||
|
||
const members = await service.smembers('test:set_remove');
|
||
|
||
expect(members).toHaveLength(2);
|
||
expect(members).toContain('member1');
|
||
expect(members).toContain('member3');
|
||
expect(members).not.toContain('member2');
|
||
});
|
||
|
||
it('should return empty array for non-existent sets', async () => {
|
||
if (skipIfRedisUnavailable()) {
|
||
console.log('跳过测试:Redis服务器不可用');
|
||
return;
|
||
}
|
||
|
||
const members = await service.smembers('test:nonexistent_set');
|
||
|
||
expect(members).toEqual([]);
|
||
});
|
||
|
||
it('should handle duplicate set members', async () => {
|
||
if (skipIfRedisUnavailable()) {
|
||
console.log('跳过测试:Redis服务器不可用');
|
||
return;
|
||
}
|
||
|
||
await service.sadd('test:duplicate_set', 'member1');
|
||
await service.sadd('test:duplicate_set', 'member1'); // 重复添加
|
||
await service.sadd('test:duplicate_set', 'member2');
|
||
|
||
const members = await service.smembers('test:duplicate_set');
|
||
|
||
expect(members).toHaveLength(2);
|
||
expect(members).toContain('member1');
|
||
expect(members).toContain('member2');
|
||
});
|
||
});
|
||
|
||
describe('数据持久性和一致性', () => {
|
||
it('should persist data across operations', async () => {
|
||
if (skipIfRedisUnavailable()) {
|
||
console.log('跳过测试:Redis服务器不可用');
|
||
return;
|
||
}
|
||
|
||
// 设置多种类型的数据
|
||
await service.set('test:persist:string', 'persistent string');
|
||
await service.set('test:persist:number', '42');
|
||
await service.sadd('test:persist:set', 'set_member');
|
||
await service.incr('test:persist:counter');
|
||
|
||
// 验证数据持久性
|
||
const stringValue = await service.get('test:persist:string');
|
||
const numberValue = await service.get('test:persist:number');
|
||
const setMembers = await service.smembers('test:persist:set');
|
||
const counterValue = await service.get('test:persist:counter');
|
||
|
||
expect(stringValue).toBe('persistent string');
|
||
expect(numberValue).toBe('42');
|
||
expect(setMembers).toContain('set_member');
|
||
expect(counterValue).toBe('1');
|
||
});
|
||
|
||
it('should maintain data consistency during concurrent operations', async () => {
|
||
if (skipIfRedisUnavailable()) {
|
||
console.log('跳过测试:Redis服务器不可用');
|
||
return;
|
||
}
|
||
|
||
// 并发执行多个操作
|
||
const promises = [];
|
||
for (let i = 0; i < 10; i++) {
|
||
promises.push(service.incr('test:concurrent:counter'));
|
||
promises.push(service.sadd('test:concurrent:set', `member${i}`));
|
||
}
|
||
|
||
await Promise.all(promises);
|
||
|
||
// 验证结果一致性
|
||
const counterValue = await service.get('test:concurrent:counter');
|
||
const setMembers = await service.smembers('test:concurrent:set');
|
||
|
||
expect(parseInt(counterValue)).toBe(10);
|
||
expect(setMembers).toHaveLength(10);
|
||
});
|
||
});
|
||
|
||
describe('清空操作', () => {
|
||
it('should clear all data with flushall', async () => {
|
||
if (skipIfRedisUnavailable()) {
|
||
console.log('跳过测试:Redis服务器不可用');
|
||
return;
|
||
}
|
||
|
||
// 设置一些测试数据
|
||
await service.set('test:flush1', 'value1');
|
||
await service.set('test:flush2', 'value2');
|
||
await service.sadd('test:flush_set', 'member');
|
||
|
||
// 清空所有数据
|
||
await service.flushall();
|
||
|
||
// 验证数据已清空
|
||
const value1 = await service.get('test:flush1');
|
||
const value2 = await service.get('test:flush2');
|
||
const setMembers = await service.smembers('test:flush_set');
|
||
|
||
expect(value1).toBeNull();
|
||
expect(value2).toBeNull();
|
||
expect(setMembers).toEqual([]);
|
||
});
|
||
});
|
||
|
||
describe('错误处理和边界情况', () => {
|
||
it('should handle empty string keys and values', async () => {
|
||
if (skipIfRedisUnavailable()) {
|
||
console.log('跳过测试:Redis服务器不可用');
|
||
return;
|
||
}
|
||
|
||
await service.set('', 'empty key');
|
||
await service.set('empty_value', '');
|
||
|
||
const emptyKeyValue = await service.get('');
|
||
const emptyValue = await service.get('empty_value');
|
||
|
||
expect(emptyKeyValue).toBe('empty key');
|
||
expect(emptyValue).toBe('');
|
||
});
|
||
|
||
it('should handle very long keys and values', async () => {
|
||
if (skipIfRedisUnavailable()) {
|
||
console.log('跳过测试:Redis服务器不可用');
|
||
return;
|
||
}
|
||
|
||
const longKey = 'test:' + 'a'.repeat(1000);
|
||
const longValue = 'b'.repeat(10000);
|
||
|
||
await service.set(longKey, longValue);
|
||
const result = await service.get(longKey);
|
||
|
||
expect(result).toBe(longValue);
|
||
});
|
||
|
||
it('should handle special characters in keys and values', async () => {
|
||
if (skipIfRedisUnavailable()) {
|
||
console.log('跳过测试:Redis服务器不可用');
|
||
return;
|
||
}
|
||
|
||
const specialKey = 'test:特殊字符:🚀:key';
|
||
const specialValue = 'Special value with 特殊字符 and 🎉 emojis';
|
||
|
||
await service.set(specialKey, specialValue);
|
||
const result = await service.get(specialKey);
|
||
|
||
expect(result).toBe(specialValue);
|
||
});
|
||
});
|
||
|
||
describe('性能测试', () => {
|
||
it('should handle multiple operations efficiently', async () => {
|
||
if (skipIfRedisUnavailable()) {
|
||
console.log('跳过测试:Redis服务器不可用');
|
||
return;
|
||
}
|
||
|
||
const startTime = Date.now();
|
||
const operations = 100;
|
||
|
||
// 执行大量操作
|
||
const promises = [];
|
||
for (let i = 0; i < operations; i++) {
|
||
promises.push(service.set(`test:perf:${i}`, `value${i}`));
|
||
}
|
||
await Promise.all(promises);
|
||
|
||
// 读取所有数据
|
||
const readPromises = [];
|
||
for (let i = 0; i < operations; i++) {
|
||
readPromises.push(service.get(`test:perf:${i}`));
|
||
}
|
||
const results = await Promise.all(readPromises);
|
||
|
||
const endTime = Date.now();
|
||
const duration = endTime - startTime;
|
||
|
||
// 验证结果正确性
|
||
expect(results).toHaveLength(operations);
|
||
results.forEach((result, index) => {
|
||
expect(result).toBe(`value${index}`);
|
||
});
|
||
|
||
// 性能检查(应该在合理时间内完成)
|
||
expect(duration).toBeLessThan(5000); // 5秒内完成
|
||
}, 10000);
|
||
});
|
||
}); |