Gradient Image Processing is developed with the strategy of maintaining product quality. Automated tests around business logic and documentation coverage are the means we use to assure this quality. This section describes actions taken to maintain quality at a high level. It contains three parts:
1. Test coverage
2. Documentation coverage
3. Part of the code
Every section contains more details about these actions.
Gradient Image Processing is covered with the integration tests contained in .spec files for each service, and end-to-end tests also. All tests are implemented and executed using ts-jest.
In the screenshot below, one can see that Gradient Image Processing has a 96.84% test coverage .
Documentation coverage is done by commenting on every method on controllers, services, repositories, etc. what each method is about. Documentation is generated using compodoc library. You can see an example of documentation coverage in the screenshot below. To read the full documentation, open it here.
In this section one part of code implementation with its test implementation is presented. The first example presents the code implementation of one service, and the second presents the test implementation of that service.
The example below shows the code implementation of one of the Gradient Image Processing services called "Image Processor Service". Its responsibility is to handle initial coordination between services for operations related to the processing of images. Initial coordination means sending the original image to saving, compression, scaling, and removal. The service is Injectable, and it uses two additional services needed in the "processImage" method.
"Image Processor Service" also handles coordination in the retry operation process. The retry process occurs when saving an original image to the storage has failed, or when an image compression and scaling has failed. When saving the original image fails, only a request to "File Service" about saving the original image is sent. When compression and scaling of an image fails, only a request to the "Image Compression and Scaling Service" is sent.
For this communication, worker queues are utilized. For every decision made about the image processing coordination, a notification to Event Bus is pushed.
Finally, this example shows in which format class and methods are commented, so that given comments provide method's and class's purposes.
/**
* Service that handles processing operations with images.
*/
@Injectable()
export class ImageProcessorService {
/**
* Dependency injection.
*/
constructor(
private queueControlService: QueueControlService,
private eventBusControlService: EventBusControlService
) {
this.queueControlService.listenToQueue(QueueName.IMAGE_PROCESSOR, async (message: ImageProcessorQueueMessage) => {
await this.processImage(message);
});
}
/**
* Method that processes the image. Send original to file service for saving and then push the image with all configurations
* to the image compression service.
*/
async processImage(msg: ImageProcessorQueueMessage | RemoveImageProcessorQueueMessage): Promise<void> {
if (msg.type === ImageProcessorQueueMessageType.SAVE_IMAGE) {
const message: ImageProcessorQueueMessage = msg as ImageProcessorQueueMessage;
if (message.retryFocus === RetryFocus.NO_RETRY || message.retryFocus === RetryFocus.RETRY_ARCHIVE) {
const imageFileQueueMessage: ImageFileQueueMessage = {
type: ImageFileQueueMessageType.SAVE_IMAGE,
imageData: message.imageInfo.imageData,
storageType: StorageType.ARCHIVE,
progressEventBusName: message.imageInfo.progressEventBusName,
imageIdentifier: message.imageIdentifier
}
this.queueControlService.enqueueMessage(QueueName.IMAGE_FILE, imageFileQueueMessage);
this.eventBusControlService.pushToEventBus<EventBusMessage>(message.imageInfo.progressEventBusName, {
type: EventBusMessageType.IMAGE_QUEUED_FOR_ARCHIVING,
imageIdentifier: message.imageIdentifier
});
}
if (message.retryFocus === RetryFocus.NO_RETRY || message.retryFocus === RetryFocus.RETRY_COMPRESSION_AND_SCALING) {
const imageConfigurationAndScalingQueueMessage: ImageCompressionAndScalingQueueMessage = {
imageInfo: message.imageInfo,
imageIdentifier: message.imageIdentifier,
configurations: message.configurations
};
this.queueControlService.enqueueMessage(QueueName.IMAGE_COMPRESSION_AND_SCALING, imageConfigurationAndScalingQueueMessage);
this.eventBusControlService.pushToEventBus<EventBusMessage>(message.imageInfo.progressEventBusName, {
type: EventBusMessageType.IMAGE_QUEUED_FOR_COMPRESSION_AND_SCALING,
imageIdentifier: message.imageIdentifier
});
}
} else {
const message: RemoveImageProcessorQueueMessage = msg as RemoveImageProcessorQueueMessage;
this.queueControlService.enqueueMessage<RemoveImageFileQueueMessage>(QueueName.IMAGE_FILE, {
type: ImageFileQueueMessageType.REMOVE_IMAGE,
storageInfo: message.storageInfo,
storageType: message.storageType,
imageIdentifier: message.imageIdentifier
});
}
}
}
The example below presents a .spec file for the service described in the example above. In this example, one can see a test implementation written to assure the quality and functionality of "Image Processor Service". Test implementation includes:
describe('ImageProcessorService', () => {
let service: ImageProcessorService;
let queueControlService: QueueControlService;
let eventBusControlService: EventBusControlService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [QueueModule, EventBusModule],
providers: [ImageProcessorService],
}).compile();
service = module.get<ImageProcessorService>(ImageProcessorService);
queueControlService = module.get<QueueControlService>(QueueControlService);
eventBusControlService = module.get<EventBusControlService>(EventBusControlService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('should process image', async () => {
let imageData: ImageData = {
imageContent: null,
mimeType: 'image/jpeg',
};
await new Promise<void>((resolve) => {
fs.readFile(process.cwd() + '/test-config/test-images/2_Blog_UniSpan.jpeg', async (err, data) => {
imageData.imageContent = data;
});
resolve();
});
const eventBusName = eventBusControlService.openEventBus();
const processorQueueMessage: ImageProcessorQueueMessage = {
type: ImageProcessorQueueMessageType.SAVE_IMAGE,
imageInfo: {
imageData: imageData,
progressEventBusName: eventBusName
},
imageIdentifier: {
sourceImageInfoId: 'source-image-id',
imageIdentifierType: ImageIdentifierType.SOURCE,
imageSizeConfigurationId: null,
imageTypeConfigurationId: null
},
retryFocus: RetryFocus.NO_RETRY,
configurations: [
{
name: 'Test Configuration 1',
type: {
id: 'test-type-id',
imageType: ImageType.WEBP,
qualityPercentage: 100,
},
size: {
id: 'test-size-id',
height: 100,
width: 100,
cropMethod: CropMethodType.FIT
}
},
{
name: 'Test Configuration 2',
type: {
id: 'test-type-2-id',
imageType: ImageType.PNG,
qualityPercentage: 50,
},
size: {
id: 'test-size-2-id',
height: 250,
width: 250,
cropMethod: CropMethodType.COVER
}
}
]
}
const fn = jest.fn();
eventBusControlService.listenToEventBus(eventBusName, (message: EventBusMessage) => {
if (message.type === EventBusMessageType.IMAGE_QUEUED_FOR_ARCHIVING) {
fn();
expect(message.imageIdentifier.sourceImageInfoId).toBe('source-image-id');
expect(message.imageIdentifier.imageIdentifierType).toBe(ImageIdentifierType.SOURCE);
expect(message.imageIdentifier.imageTypeConfigurationId).toBe(null);
expect(message.imageIdentifier.imageSizeConfigurationId).toBe(null);
} else {
fn();
expect(message.type).toBe(EventBusMessageType.IMAGE_QUEUED_FOR_COMPRESSION_AND_SCALING);
expect(message.imageIdentifier.sourceImageInfoId).toBe('source-image-id');
expect(message.imageIdentifier.imageIdentifierType).toBe(ImageIdentifierType.SOURCE);
expect(message.imageIdentifier.imageSizeConfigurationId).toBe(null);
expect(message.imageIdentifier.imageTypeConfigurationId).toBe(null);
}
});
let countOfListeners = 0;
const promiseCompression = new Promise<void>((resolve) => {
queueControlService.listenToQueue(QueueName.IMAGE_COMPRESSION_AND_SCALING,
async (queueMessage: ImageCompressionAndScalingQueueMessage) => {
expect(queueMessage.imageInfo.progressEventBusName).toBe(eventBusName);
expect(queueMessage.imageIdentifier.sourceImageInfoId).toBe('source-image-id');
expect(queueMessage.configurations[0].type.imageType).toBe(ImageType.WEBP);
expect(queueMessage.configurations[0].type.qualityPercentage).toBe(100);
expect(queueMessage.configurations[0].type.id).toBe('test-type-id');
expect(queueMessage.configurations[1].size.cropMethod).toBe(CropMethodType.COVER);
expect(queueMessage.configurations[1].size.width).toBe(250);
expect(queueMessage.configurations[1].size.height).toBe(250);
expect(queueMessage.configurations[1].size.id).toBe('test-size-2-id');
resolve();
});
countOfListeners++;
if (countOfListeners === 2) {
queueControlService.enqueueMessage(QueueName.IMAGE_PROCESSOR, processorQueueMessage);
}
});
const promiseImageFile = new Promise<void>((resolve) => {
queueControlService.listenToQueue(QueueName.IMAGE_FILE,
async (queueMessage: ImageFileQueueMessage) => {
expect(queueMessage.imageData).toBe(imageData);
expect(queueMessage.imageIdentifier.sourceImageInfoId).toBe('source-image-id');
expect(queueMessage.imageIdentifier.imageIdentifierType).toBe(ImageIdentifierType.SOURCE);
expect(queueMessage.imageIdentifier.imageTypeConfigurationId).toBe(null);
expect(queueMessage.imageIdentifier.imageSizeConfigurationId).toBe(null);
expect(queueMessage.progressEventBusName).toBe(eventBusName);
expect(queueMessage.storageType).toBe(StorageType.ARCHIVE);
resolve();
});
countOfListeners++;
if (countOfListeners === 2) {
queueControlService.enqueueMessage(QueueName.IMAGE_PROCESSOR, processorQueueMessage);
}
});
await promiseCompression;
await promiseImageFile;
expect(fn.mock.calls.length).toBe(2);
});
it('should properly pass image removal message to file service', async () => {
queueControlService.listenToQueue<RemoveImageFileQueueMessage>(QueueName.IMAGE_FILE, async (message) => {
expect(message.type).toBe(ImageFileQueueMessageType.REMOVE_IMAGE);
expect(message.storageType).toBe(StorageType.RUNTIME);
expect(message.storageInfo).toBe('test-storage-info');
});
queueControlService.enqueueMessage<RemoveImageProcessorQueueMessage>(QueueName.IMAGE_PROCESSOR, {
type: ImageProcessorQueueMessageType.REMOVE_IMAGE,
storageInfo: 'test-storage-info',
imageIdentifier: {
imageIdentifierType: ImageIdentifierType.RESULT,
sourceImageInfoId: 'source-image-id',
imageSizeConfigurationId: 'size-1',
imageTypeConfigurationId: 'type-1'
},
storageType: StorageType.RUNTIME
});
});
});