Everything as a code: Part 2

Kostiantyn Ivanov
8 min readNov 12, 2023

--

Previous part: https://medium.com/@svosh2/everything-as-a-code-part-1-197ff3b7647a

Infrastructure as a code

The next series of tools will give us ability to describe our infrastructure as a code.

IaC is very popular approach and there are a number of useful tools on this field. One of the most popular is Terraform.

Terraform is an open-source Infrastructure as Code (IaC) tool developed by HashiCorp. It is used for defining and provisioning infrastructure and resources in a declarative manner. https://www.terraform.io/

Example of terraform code that will setup a database and API gateway for our application on AWS:

# Define the provider (AWS)
provider "aws" {
region = "us-east-1" # Replace with your desired AWS region
}

# Create a VPC
resource "aws_vpc" "example" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
}

# Create subnets for the Aurora DB cluster
resource "aws_subnet" "subnet_a" {
count = 2
vpc_id = aws_vpc.example.id
cidr_block = "10.0.1.${count.index}/24"
availability_zone = "us-east-1a" # Replace with your desired availability zone
}

# Create a security group for the Aurora DB cluster
resource "aws_security_group" "aurora_db_sg" {
name_prefix = "aurora-db-sg-"
vpc_id = aws_vpc.example.id

# Define inbound and outbound rules as needed
# Example: allow incoming traffic from your application servers
# Add your rules here...
}

# Create an Aurora database cluster
resource "aws_db_cluster" "aurora_cluster" {
cluster_identifier = "my-aurora-cluster"
engine = "aurora"
engine_version = "5.6.10a"
master_username = "admin"
master_password = "your_password"
database_name = "mydb"
backup_retention_period = 7
skip_final_snapshot = true
db_subnet_group_name = "default"
vpc_security_group_ids = [aws_security_group.aurora_db_sg.id]
availability_zones = [aws_subnet.subnet_a[0].availability_zone, aws_subnet.subnet_a[1].availability_zone]
}

# Create an Application Load Balancer (ALB)
resource "aws_lb" "example" {
name = "my-alb"
internal = false
load_balancer_type = "application"
subnets = [aws_subnet.subnet_a[0].id, aws_subnet.subnet_a[1].id]
}

# Create a target group for the ALB
resource "aws_lb_target_group" "example" {
name = "my-tg"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.example.id
}

# Register Aurora DB cluster instances as targets
resource "aws_lb_target_group_attachment" "example" {
target_group_arn = aws_lb_target_group.example.arn
target_id = aws_db_instance.example.id # Assuming you have an instance resource named aws_db_instance.example
}

# Create a listener for the ALB
resource "aws_lb_listener" "example" {
load_balancer_arn = aws_lb.example.arn
port = 80
protocol = "HTTP"

default_action {
type = "fixed-response"
fixed_response_type = "200"
fixed_response_content = "Hello, world!"
}
}

# Output the DNS name of the Aurora cluster endpoint
output "aurora_cluster_endpoint" {
value = aws_db_cluster.aurora_cluster.endpoint
}

It’s pretty powerful tool that support all the popular cloud providers as well as your custom implementations.

Other IaC alternatives:

Ansible is an open-source automation tool and configuration management system designed to simplify the management of IT infrastructure, application deployment, and task automation. https://www.ansible.com/

Provider-specific alternatives:
AWS Cloud Development Kit (CDK)
is an open-source software development framework that makes it easier to define cloud infrastructure in code using familiar programming languages. The CDK provides developers with a higher-level, more abstracted way to define and provision AWS resources, as opposed to manually writing low-level CloudFormation templates or using AWS Management Console.

Azure ARM Templates used to define and provision Azure resources and services as code. These templates are a critical component of infrastructure as code (IaC) in Microsoft Azure. ARM templates enable you to describe your desired cloud infrastructure in a declarative manner, making it possible to create, modify, and delete Azure resources consistently and predictably.

Google Cloud Deployment Manager is a Google Cloud service that allows you to define, deploy, and manage cloud resources using declarative configuration files. It’s an Infrastructure as Code (IaC) tool that helps automate the creation and management of Google Cloud Platform (GCP) resources.

Advantages of having IaC:

Infrastructure as Code (IaC) offers numerous advantages that contribute to more efficient, reliable, and scalable management of IT infrastructure and resources.

Automation: we don’t need to setup our infrastructure manually.

Consistency: our infrastructure is consistent between environments.

Version Control: we always can track what was changed.

Testing as a code

The idea of this approach is to automate your testing as much as possible.

Unit tests

Unit tests are a type of software testing in which individual components or functions of a software application are tested in isolation to ensure they perform as expected. The purpose of unit testing is to validate that each unit of the software code, often at the level of individual functions or methods, behaves correctly in response to different inputs and conditions.

Unit tests usually are covered pretty well and the framework/library to automate them should be decided based on your technology stack.

Here are a few popular of them:

JUnit is a widely used open-source testing framework for Java programming language. https://junit.org/junit5/
Example of code:


import org.junit.Test;
import static org.junit.Assert.*;

public class PaymentServiceTest {

private PaymentService paymentService = new PaymentService();

@Test
public void testSuccessfulPayment() {

// Test processing a successful payment
boolean result = paymentService.processPayment(50.0, "1234-5678-9012-3456");

// Assert that the payment was successful
assertTrue(result);
}

@Test
public void testFailedPayment() {

// Test processing a failed payment (for example, insufficient funds)
boolean result = paymentService.processPayment(200.0, "9876-5432-1098-7654");

// Assert that the payment failed
assertFalse(result);
}

// You can add more test methods for different scenarios

}

NUnit is an open-source testing framework for the .NET framework and .NET Core. https://nunit.org/
Example of code:

using NUnit.Framework;

[TestFixture]
public class PaymentServiceTests
{
[Test]
public void ProcessPayment_SuccessfulPayment_ReturnsTrue()
{
// Arrange
var paymentService = new PaymentService();

// Act
bool result = paymentService.ProcessPayment(50.0, "1234-5678-9012-3456");

// Assert
Assert.IsTrue(result);
}

[Test]
public void ProcessPayment_FailedPayment_ReturnsFalse()
{
// Arrange
var paymentService = new PaymentService();

// Act
bool result = paymentService.ProcessPayment(200.0, "9876-5432-1098-7654");

// Assert
Assert.IsFalse(result);
}

// You can add more test methods for different scenarios
}

Jasmine. In the JavaScript ecosystem, particularly for frontend testing, the most commonly used testing framework is Jasmine. Jasmine is a behavior-driven development (BDD) testing framework for JavaScript. https://jasmine.github.io/
Example of usage for angular:

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyComponent } from './my.component';

describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;

beforeEach(() => {
TestBed.configureTestingModule({
declarations: [MyComponent],
});

fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
});

it('should create the component', () => {
expect(component).toBeTruthy();
});

it('should have a default title', () => {
expect(component.title).toBe('Default Title');
});

// Add more test cases for component behavior
});

Integration tests

Integration tests are a type of software testing that focuses on verifying the interactions and communication between different components or modules of a system.

Integration tests can be implemented separately from the business logic code using different programming language. But would be more straightforward to support only one language per project (or at less don’t add one more of them only because of tests).

Here are some popular integration tests frameworks:

TestNG (Java) is a testing framework inspired by JUnit and NUnit but designed to address some limitations of JUnit. TestNG supports parallel test execution and has features that are beneficial for integration testing, such as data-driven testing and parameterization. https://testng.org/doc/
Example of usage:

import org.testng.Assert;
import org.testng.annotations.Test;

public class PaymentsApiTest {

// Replace these values with actual API details
private static final String API_BASE_URL = "https://api.example.com/payments";
private static final String API_KEY = "your-api-key";

@Test
public void testProcessPayment() {
// Implement the test for processing a payment
// You may need to mock/stub the API calls or use a test environment

// Example:
PaymentsApi paymentsApi = new PaymentsApi(API_BASE_URL, API_KEY);
PaymentRequest paymentRequest = new PaymentRequest(/* fill with appropriate data */);
PaymentResponse paymentResponse = paymentsApi.processPayment(paymentRequest);

Assert.assertNotNull(paymentResponse);
Assert.assertEquals(paymentResponse.getStatus(), "success");
}

@Test
public void testGetPaymentStatus() {
// Implement the test for getting payment status
// You may need to mock/stub the API calls or use a test environment

// Example:
PaymentsApi paymentsApi = new PaymentsApi(API_BASE_URL, API_KEY);
String paymentId = "123456";
PaymentStatus paymentStatus = paymentsApi.getPaymentStatus(paymentId);

Assert.assertNotNull(paymentStatus);
Assert.assertEquals(paymentStatus.getStatus(), "completed");
}

// You can add more tests for other API functionalities as needed
}

Behave is a popular Python framework for behavior-driven development (BDD). Behavior-driven development is an agile software development methodology that encourages collaboration between developers, QA, and non-technical stakeholders to define and understand the behavior of a system through the use of natural language specifications. https://behave.readthedocs.io/en/latest/
Example of usage:

# features/steps/payments_steps.py

import requests
from behave import given, when, then

PAYMENTS_API_URL = "https://api.example.com/payments"

@given('the payment service is available')
def step_impl(context):
# You can perform any setup needed for the payment service
pass

@when('I request to process a payment with amount ${amount:d}')
def step_impl(context, amount):
# Make a request to the payments API to process the payment
payload = {"amount": amount}
response = requests.post(PAYMENTS_API_URL, json=payload)
context.response = response

@then('the payment is processed successfully')
def step_impl(context):
# Check if the payment was processed successfully based on the API response
assert context.response.status_code == 200
assert context.response.json().get("status") == "success"

UI tests

UI (User Interface) tests are a type of software testing that focuses on validating the functionality and behavior of a software application’s user interface. UI tests often involve testing the entire application or a significant portion of it from the user’s perspective. This includes interactions between different components and the overall flow of the application.

Some of the frameworks can help you with automation of UI tests:

Selenium is an open-source framework for automating web browser interactions. It provides a suite of tools and libraries for controlling web browsers programmatically, allowing developers and testers to automate the testing of web applications across various browsers and platforms. Selenium supports multiple programming languages, including Java, Python, C#, Ruby, and others. https://www.browserstack.com/selenium
Example of code on java:

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

public class PaymentPageTest {

private WebDriver driver;
private String baseUrl = "https://example.com/payment"; // Replace with your actual URL

@BeforeClass
public void setUp() {
// Set the path to the ChromeDriver executable
System.setProperty("webdriver.chrome.driver", "path/to/chromedriver");
driver = new ChromeDriver();
driver.get(baseUrl);
}

@Test
public void testPaymentProcess() {
// Fill in payment details
WebElement cardNumberInput = driver.findElement(By.id("cardNumber"));
cardNumberInput.sendKeys("1234567890123456");

WebElement expirationDateInput = driver.findElement(By.id("expirationDate"));
expirationDateInput.sendKeys("12/23");

WebElement cvvInput = driver.findElement(By.id("cvv"));
cvvInput.sendKeys("123");

// Submit the form
WebElement submitButton = driver.findElement(By.id("submitButton"));
submitButton.click();

// Wait for the payment to be processed (you may need to add explicit waits)
// Example: WebDriverWait wait = new WebDriverWait(driver, 10);
// wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("paymentSuccessMessage")));

// Verify the payment success message
WebElement successMessage = driver.findElement(By.id("paymentSuccessMessage"));
Assert.assertTrue(successMessage.isDisplayed());
}

@AfterClass
public void tearDown() {
// Close the browser
if (driver != null) {
driver.quit();
}
}
}

Cypress is an open-source end-to-end testing framework designed for web applications. It enables developers to write and run tests directly in the browser, providing fast and reliable testing for modern web applications.
Example of code:

// Example Cypress test
describe('Payment Page Test', () => {
it('Successfully processes payment', () => {
cy.visit('https://example.com/payment'); // Replace with your actual URL

cy.get('#cardNumber').type('1234567890123456');
cy.get('#expirationDate').type('12/23');
cy.get('#cvv').type('123');

cy.get('#submitButton').click();

// Wait for the payment success message
cy.get('#paymentSuccessMessage').should('be.visible');
});
});

Tests as code Summary:

Making our testing as a code gives us ability to include our test scenario to CICD pipelines, be more consistent and reliable in our tests approaches.

Summary:

We discovered a set of useful approches that could make you an “Everything as code” adept =) Where are you on this road to the dream?

--

--