• Trg Slobode 16, Tuzla, 75000, Bosnia and Herzegovina
  • info@gradient.ba
Schedule informative meeting Contact Us
Gradient Software Development Logo
  • Home
  • Services
    • Software Development Partnership
    • Business Solution Development
    • Software Architecture Design and Consulting
  • Products
    • Gradient Image Processing
  • Team
    • Azra Kunić
    • Emir Aličić
  • About us
  • Pricing Models
  • Contact us

Product Implementation Quality

Product Implementation Quality

Summary

  1. Product quality
    1. Test coverage
    2. Documentation coverage
    3. Code example

1. Product Quality

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.

1.1. Test coverage

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 .

View Picture in Full Size

1.2. Documentation 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.

View Picture in Full Size

1.3. Code example

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:

  • assuring functionality of sending the image for saving, compression, and scaling,
  • assuring functionality of sending the image for removal.


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
    });
  });
});

footer-logo

Trg Slobode 16, Tuzla 75000

Bosnia and Herzegovina

info@gradient.ba

Services

  • Software Development Partnership
  • Business Solution Development
  • Software Architecture Design and Consulting

Get Information

  • About us
  • Make an Appointment
  • Contact us

Social

  • Facebook
  • LinkedIn
  • UpWork

Legal

  • Privacy Policy
  • Cookies Policy
  • Terms and Conditions
Copyright © 2022 Gradient.ba