It is very easy to write a program that is understood by computers, but it is not easy to write a program that is easy to understand by other developers. Most codes are written once and read many times, so it is very important to write them as cleanly as possible. It might benefit others and also for future you that reading your code. Remember there are no hard rules in software development, so no point in arguing with others to prove that your opening is the best. If there is only one best to do anything why do people implement the same thing in many ways? that’s because everything has tradeoffs you only can decide what you can tolerate and what not depending on your context. With that in mind let’s start the discussion on names. There is a famous saying by Phil Karlton “There are only two hard things in Computer Science: cache invalidation and naming things.”. I’m sure that Every developer has been in a situation where they struggled to name something in their code. why? because there is no right name, it all depends on personal preferences, context, and many other factors. But we don’t need to write code to understand by whole world. We need some conventions that the working team gets comfortable with and easy to onboard a new member to the team. There is a famous article written by Tim Ottinger called Ottinger’s Rules for Variable and Class Naming which is the base of many resources you will find on naming best practices. People describe it in many ways by adding their own opinions and experiences Remember the reason we need clean maintainable code is that there is no hard end of development, requirements get added, and existing ones change due to many uncontrollable reasons. Good naming is not enough to have maintainable clean code, the structure is also very important. Here I will discuss some principles that you can use while naming and structuring code. Please don’t lie. This is the most important message I want to convey in this article. All other tips will help you to achieve this goal. One of the major benefits of writing clean code is when there is a change you should be able to do without reading lots of unrelated code pieces to understand the whole application at once. Think of a simple example, you are calling a simple getter to get a property value and then you realize there a database update operation is hidden inside that getter. Then you will never call any function without reading it. The trust is broken. There is no value in having clean code after this trust is broken. Good names and structure help you to navigate to the place you need to make the change without reading a hell lot of code. It’s fine to have long names. Comments are not the solution, developers do not read comments but they will have to read the code. Why do we need comments when we are writing code also using plain English? Comments can be useful to say why you do something in that way, not what code does. The code should speak for itself. Does not matter how well you craft the code if it’s not telling what it does. Most times lies are in function names. Especially when a side effect is executed when you run a function name that says it gives you something. The principle Command Query Separation which is also known as CQS can be used to write clean functions. Queries return some value while not changing the observable state of the system and commands make changes to the system state and do not return anything. If you can separate those two very good but reality is not that nice. So in such cases mention it in the function name. There is no better documentation than code which is the actual implementation. One obvious benefit of CQS by looking at the signature you can say whether it has side effects to not. One tip to convert queries that return something is to pass a callback function that deals with that return value which makes the query return nothing. Spend some time when you name something which will save you a lot of time in the future. Developers are very excited about new technology trends frameworks and programming languages and like to spend many hours learning them. But when they use them they are very rushed. Nobody is interested in investing time to revisit your variable and function names. If you want to do this fast you have to practice it, spend some time at the beginning and surely it will become natural to you later. The best way is to communicate your intent. Use Domain-specific words that are used in normal communication. It will reduce the gap between your normal conversation and reading a code. Don’t try to be oversmart by shortening names into nice acronyms. Over time these names become very confusing. Using long names is not a bad thing. Long functions might have a hidden class The main reason functions get long is they are reusing function-scoped variables throughout the function. That also means the function is doing many interconnected things. Isn’t that what do use classes in object-oriented languages? Even procedural languages have mechanisms to create scoped modules. So next time when you see a long function try to identify hidden classes inside it and do the extraction. The relationship between the length of name and execution scope For variables use long descriptive names when the execution scope is large. For example, class-level variables need to have full names. If you use single-letter variables in the functions you might not understand what it is and you will have to go back to the declaration to identify. If you need to go to the declaration to understand what a variable is that name is bad. But for finer scopes, it is
The Optional class was introduced in Java 8 as a key element in the stream API. Let’s first understand the problem that was caused to introduce this class. The problem The stream API was introduced in Java 8 and I assume every Java developer has used this API in their datoday development. Let’s consider a simple example. List<String> names = List.of(“vinod”, “madubashana”); names.stream() .filter(name -> name.startsWith(“a”)) .findFirst(); Now that you are designing the stream API, what should be the return value in the above case? No option other than returning null. The streams can be empty or they can become empty in the process of executing intermediate operations like filters as in the above case. But if it is designed to return null the API does not indicate that it can be null. What if you need to explicitly express that the return value can be empty through the API? The Solution The only solution is to introduce a new wrapper class and that’s what is provided natively in the Java 8 using Optional class. It is not a solution to issues related to nulls, it is intended to represent return types of our APIs to indicate that it can be empty which will require care when dealing with them. Before Java 8 we wrote our implementations but now we don’t need to do so. So basically Optional class provides an API to represent a value which also can be empty. Construction Optional is a final class and hence we can’t subclass it. The constructors are also private and static factor methods are provided to create instances. Optional.empty(); Optional.of(“vinod”); Optional.ofNullable(null); The empty method can be used to create empty optional. Never use nulls to reference an Optional instance, always use the empty method to create an empty Optional. If you return a null from a method where the return type is an Optional the whole purpose of introducing Optional is a waste. The of method can be only used to wrap a non-null reference. If a null value is passed a NullPointerException will be thrown. The ofNullable method allows to passing of null references. That does not mean that you can consume a null value from an Optional. It allows you to pass a null value but when you pass a null value it returns an empty Optional, not an Option which contains a null value. Java Serialization This is not a serializable class. So you can’t use Java serialization. This is intentionally done to support value objects which is a part of Project Valhalla, in the near future. Specialized Optionals To avoid boxing and unboxing specialized Optional classes are available like OptionalInt, OptionalLong, and OptionalDouble. Use these classes when applicable for better performance. Consume an Optional String value = optional.get(); if (optional.isPresent()) { String value1 = optional.get(); } optional.ifPresent(val -> System.out.println(val)); optional.ifPresentOrElse(System.out::println, () -> System.out.println(“no value”));String alternative = optional.orElse(“alternative”); String alternative1 = optional.orElseGet(() -> “alternative”); Optional<String> alternativeOptional = optional.or( () -> Optional.of(“alternative”));String value2 = optional.orElseThrow(); String value3 = optional.orElseThrow( () -> new RuntimeException(“exception”)); The API is self-exploratory. Let’s understand some important points regarding using these methods. The get method is the simplest way and the way you try to avoid always. This will throw NoSuchElementException when you call it for empty optional. This also suggests that this is not a solution for handling NullPointerException. It is always need to call isPresent or isEmpty methods to check the emptiness and then call the get method. But still, there are other better alternative ways available. ifPresent method simplifies this check and concisely operates using a consumer. The consumer will be only invoked if the optional is non-empty. ifPresentOrElse accepts a Runnable as a second argument which can be used to run some logic when optional is empty. orElse and orElseGet can be used to get a default value when the optional is empty. or method becomes handy when your return type is optional and you need to return a new option when the planned option to return becomes empty. Finally, it is easy to throw runtime exceptions using orElseThrow methods. Operations Another advantage of having Optional rather than returning null is the ability to run transformation operations without null checks. It provides the ability to write a fluent chain of method calls. See the below example. String name = optional.filter(name -> name.startsWith(“ab”)) .map(String::toUpperCase) .orElse(null); Hope this is also self-exploratory. Another operation available is flatmap which can be used to flatten nested Optionals similar to flatmap operation in stream API. The stream method is also available which can convert Optional into a Java stream. Then it is possible to use all the stream-related operations. Best Practices The intended and best place to use it is in the method return type. You might already experience this when using many open-source libraries. Not suitable for method arguments and constructor arguments. This is because it clutters the client code and adds unnecessary complexity. Not suitable as collection elements. This also makes it harder to read when we write the consume operations in stream pipelines. Don’t wrap collections using optional. Instead, return an empty collection object by using some helper methods like Collections.emptyList() Don’t try to be clever using tricks like wrapping objects that can be null and the use optional class API to handle the code, simple null checks will do the same thing for you which yields much more readable code. No suitable class fields. This is an overuse of optional. The optional is a wrapper which means it consumes more memory than the normal references. Overusing them can slow down your application. If the using class is serializable then we have another problem because optional are not serializable. Instead, you can return optional from the getter methods to indicate that fields can be empty. Try to avoid nested optional which makes code unreadable when consuming such optional. Don’t use identity-sensitive operations like equality check using == which can give unpredictable results. The equals method works as expected by comparing values inside optional.
As Java Spring developers, testing interactions with AWS services, especially within the context of AWS Lambda function handlers, can be a challenging aspect of our projects. LocalStack comes to the rescue by providing a powerful solution for testing AWS cloud services locally, eliminating the need for a live AWS environment. In this comprehensive guide, we’ll delve into the intricacies of setting up and effectively using LocalStack for testing AWS Lambda function handlers with SQS events in a Java Spring project. Understanding LocalStack What is LocalStack? https://www.localstack.cloud/ LocalStack stands out as a lightweight, self-contained AWS cloud stack designed specifically for local development and testing. It presents a fully functional local environment that mirrors the behavior of AWS cloud services. This makes LocalStack an ideal choice for developers seeking to validate their applications’ interactions with AWS services without the overhead of deploying to an actual AWS environment. Prerequisite: Docker Installation for Linux Ubuntu Before setting up LocalStack, ensure that Docker is installed on your Linux Ubuntu machine. Follow these steps to install Docker: Update Package Lists: Open a terminal and update the package lists: sudo apt update Install Docker Dependencies: Install packages to allow apt to use a repository over HTTPS: sudo apt install apt-transport-https ca-certificates curl software-properties-common Add Docker’s Official GPG Key: Add Docker’s official GPG key to ensure the integrity of the packages: curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg –dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg Set Up the Stable Docker Repository: Set up the stable Docker repository: echo “deb [signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable” | sudo tee /etc/apt/sources.list.d/docker.list > /dev/nul Install Docker Engine: Update the package lists once more, then install the Docker engine: sudo apt update sudo apt install docker-ce docker-ce-cli containerd.io Verify Docker Installation: Verify that Docker is installed correctly by running: sudo docker –version You should see information about the Docker version. Now that Docker is installed, you’re ready to proceed with LocalStack setup. How LocalStack Works Under the Hood LocalStack works by emulating AWS cloud services locally using Docker containers. Here’s an overview of how it operates under the hood: Docker Containers: LocalStack uses Docker containers to encapsulate and emulate various AWS services locally. Each AWS service supported by LocalStack is typically represented by a separate Docker container. Service Endpoints: LocalStack exposes service endpoints for each emulated AWS service. These endpoints mimic the behavior of the corresponding AWS services. For example, there are endpoints for local DynamoDB, SQS, S3, etc. Java SDK Compatibility: LocalStack is compatible with the AWS SDK for Java, allowing Java applications to interact with emulated AWS services using the standard Java SDK. Service Initialization: When LocalStack starts, it initializes the Docker containers for the emulated AWS services. This involves setting up configurations and data storage. AWS Service Emulation: LocalStack intercepts and handles requests made to its service endpoints, emulating the behavior of various AWS services. For instance, when a Java application makes an API call to the local DynamoDB endpoint provided by LocalStack, it receives a response as if it were a real DynamoDB service. Data Storage: LocalStack creates local storage to mimic the behavior of AWS cloud storage for services like DynamoDB and S3. This allows developers to perform CRUD operations using the AWS SDK for Java. Configuration and Customization: LocalStack provides configuration options for customizing its behavior, such as specifying which AWS services to emulate and configuring service endpoints. Integration with Test Frameworks: LocalStack seamlessly integrates with testing frameworks like JUnit, enabling developers to incorporate local AWS service emulation into their test suites. In summary, LocalStack provides a local environment that closely simulates AWS cloud services through Docker containers, allowing developers to test and validate their applications’ interactions with AWS services without the need for a live AWS environment. Setting up LocalStack in a Java Spring Project Adding Dependencies Before diving into the setup, ensure your project includes the necessary dependencies. Add the following dependencies to your project’s build file, such as pom.xml: <!– Dependencies for LocalStack and Testcontainers –> <dependency> <groupId>org.testcontainers</groupId> <artifactId>localstack</artifactId> <version>LATEST_VERSION</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>junit-jupiter</artifactId> <version>LATEST_VERSION</version> <scope>test</scope> </dependency> Replace LATEST_VERSION with the latest version of Testcontainers. LocalStackSetup Class Let’s start with the LocalStackSetup class. This class initializes a LocalStack container with specific AWS services, such as DynamoDB, SQS, and Secrets Manager. Here’s an example: import com.amazonaws.auth.AWSStaticCredentialsProvider; import com.amazonaws.auth.BasicAWSCredentials; import org.testcontainers.containers.localstack.LocalStackContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.utility.DockerImageName; import static org.testcontainers.containers.localstack.LocalStackContainer.Service.*;public class LocalstackSetup { @Container public static LocalStackContainer localStack = new LocalStackContainer(DockerImageName.parse(“localstack/localstack:latest”)) .withServices(DYNAMODB, SQS, SECRETSMANAGER); static AWSStaticCredentialsProvider localStackCredentialsProvider = new AWSStaticCredentialsProvider (new BasicAWSCredentials(localStack.getAccessKey(), localStack.getSecretKey())); } ComponentTestConfiguration Class The ComponentTestConfiguration class serves as a test configuration class that sets up beans for testing components. It starts the LocalStack container in a static block and configures beans for DynamoDB, SQS, Secrets Manager, and more. Below is an illustrative snippet: package com.base.config; import com.amazonaws.client.builder.AwsClientBuilder; import com.amazonaws.secretsmanager.caching.SecretCacheConfiguration; import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig; import com.amazonaws.services.secretsmanager.AWSSecretsManager; import com.amazonaws.services.secretsmanager.AWSSecretsManagerClientBuilder; import com.amazonaws.services.sqs.AmazonSQS; import com.amazonaws.services.sqs.AmazonSQSClientBuilder; import com.amazonaws.xray.AWSXRay; import com.base.client.PalmsClient; import com.base.component.utils.DynamoDBTestUtils; import com.base.component.utils.SecretMangerUtils; import com.base.component.utils.SqsTestUtils; import org.mockito.Mockito; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean;import java.time.Duration;import static com.base.config.LocalstackSetup.localStack; import static com.base.config.LocalstackSetup.localStackCredentialsProvider; import static org.testcontainers.containers.localstack.LocalStackContainer.Service.*;@TestConfiguration public class ComponentTestConfiguration { static { localStack.start(); } @Bean public DynamoDBTestUtils dynamoDBTestUtils() { return new DynamoDBTestUtils(); } @Bean public DynamoDBMapper dynamoDBMapper( final AmazonDynamoDB amazonDynamoDB, final DynamoDBMapperConfig dynamoDBMapperConfig ) { AWSXRay.beginSegment(“AmazonDynamoDBv2”); return new DynamoDBMapper(amazonDynamoDB, dynamoDBMapperConfig); } @Bean public AmazonDynamoDB amazonDynamoDB() { // Configures Amazon DynamoDB with LocalStack endpoint and credentials. // … return AmazonDynamoDBClientBuilder.standard() .withEndpointConfiguration(new AwsClientBuilder .EndpointConfiguration(localStack.getEndpointOverride(DYNAMODB).toString(), localStack.getRegion())) .withCredentials(localStackCredentialsProvider) .build(); } @Bean public AmazonSQS amazonSQS() { // Configures Amazon SQS with LocalStack endpoint and credentials. // … return AmazonSQSClientBuilder.standard() .withEndpointConfiguration(new AwsClientBuilder .EndpointConfiguration(localStack.getEndpointOverride(SQS).toString(), localStack.getRegion())) .withCredentials(localStackCredentialsProvider) .build(); } @Bean public PalmsClient thirdPartyClient() { return Mockito.mock(ThirdPartyClient.class); } @Bean public AWSSecretsManager awsSecretsManager() { return AWSSecretsManagerClientBuilder.standard() .withEndpointConfiguration(new AwsClientBuilder .EndpointConfiguration(localStack.getEndpointOverride(SECRETSMANAGER).toString(), localStack.getRegion())) .withCredentials(localStackCredentialsProvider) .build(); } @Bean public SecretCacheConfiguration secretCacheConfiguration(AWSSecretsManager awsSecretsManager) { return new SecretCacheConfiguration() .withClient(awsSecretsManager()) .withCacheItemTTL(Duration.ofHours(12).toMillis()); } @Bean public SqsTestUtils sqsTestUtils() { return new SqsTestUtils(); } @Bean public SecretMangerUtils secretMangerUtils() { return new SecretMangerUtils(); } } DynamoDBTestUtils Class For DynamoDB testing, the DynamoDBTestUtils class offers utility methods for creating and deleting DynamoDB tables during tests. Consider the following example: package com.base.component.utils; import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper; import
Tailwind CSS is a utility-first CSS framework that provides many ready-made classes that can be used. I would talk about it only a little, you can learn about Tailwind CSS and its power with the help of their documentation here. Their documentation is excellent👌👌👌. Since their documentation is excellent you can integrate it into any project using their guides available here. In my case, it is a react app that is created using Vite, and setup instructions are available here. and the sample project is available at this Git Hub link It’s that simple and no point in showing the same setup in here because they might change at the time that you are reading. One thing you might observe is after adding this it will add some default styles to the project. The details about these default styles and extending and disabling details are available here. The goal of this article is to provide you with two settings that can be used in VS Code to improve your productivity when working with Tailwind CSS. 1. Tailwind CSS IntelliSense Plugin Simply install this plugin using the VS code plugin manager. The first benefit you gain with this plugin is automatic completion. Since most class names are self-explaining ones this is very helpful to find the class without going through the documentation. One important feature to have is it gives you the ability to show the actual CSS code when you hover over the class name in the IDE. More details are available here. 2. Reformatting using Prettier Prettier is an opinionated code format that can be used to format your code across the team in the same way. You can learn more about usage and configuration details here. To use Prettier in VS Code install this plugin first. Then you can change settings to reformat your file when saving the file. Also, need to change the default format to Prettier Now the Prettier will reformat you when you save the file. But this is a global setting, if you need to enable this only for the project, create a folder named .vscode, and inside it add a settings.json file with the below content. { “editor.defaultFormatter”: “esbenp.prettier-vscode”, “editor.formatOnSave”: true} So far nothing specific about Tailwind. When using Tailwind the classes can grow rapidly since we are using tiny classes to build the full style. Prettier can help to some extent to handle this clutter. But still, it is harder to find a way to sort these names in the same way across the teams. Luckily the Tailwind team provides a Prittier plugin that will sort the classes automatically. Follow the instructions here to set up this plugin. After that, you can observe that class names are sorted automatically when you save the file. Before saving the file After saving the file Note: The extension od prettier.config file needs to be .cjs (not .js) in a Vite project.
Handling time feels simple in any programming language since standard time handling API is baked into the language. Most developers start to experience the complexity of date and time in a production incident. I try to make you not get into such a case with the help of this blog post. Table of Content What makes handling date and time complex? Java date time API before Java 8 The design and basic building blocks of the new date-time API Design patterns of method names Instant Clock LocalDate, LocalTime and LocalDateTime TemporalAdjuster Duration and Period Date time with timezones OffsetDateTime ZonedDateTime Regional Calendars DateTimeFormatter Accuracyr Persistence External Libraries References What makes handling date and time complex? People are used to doing their work in the daytime and sleeping at night (except for the developers😅) and also need to anticipate the seasons to occur at the correct periods of the year. Hence it is necessary to define the hours and days based on natural events like sunrise and seasons. On the other hand, standard units of measuring time need to be fixed units. The problem is natural events are not happening at a fixed rate for example the time taken by Earth to complete a cycle itself and around the sun changes slowly. So without corrections to the standard units, we will observe the drift of natural events from the normal time of the day. The primary time standard used to govern clocks and time worldwide is Coordinated Universal Time or UTC. It is within the mean solar time of roughly one second. The standard time definitions (which are based on atomic clocks which measure the time by monitoring resonant frequency of atoms) and clock synchronization are a broad topic that you can dig deeper with online resources if you are interested. As developers, we might not need to worry much about the above complications. However, I would like to highlight three points that every developer should be aware of because they are visible in the timeline. Leap Year — This correction aligns seasons to stay the same (For example winter should start at the end of the year). In a normal year, there are 365 days. But the time takes Earth to rotate around the sun is a bit less than 365.25 days. If it is not corrected over time seasons will be drifted. Hence after 4 years additional day is added in February(29th). This day also adds an over-correction and hence every 25th leap year this additional day is going to skip (which means in every 100th year). There is a final correction which is again in every 400th year an additional day is added and that is why the year 2000 is a leap year and the year 2100 will not be a leap year. The summary is all years that are exactly divisible by four are leap years, except centurial years, which are leap years if they are exactly divisible by 400. Time Zone — A time zone is an area where a uniform standard time can be found. Across time zones we find different times. This is because we all like to start our work in the morning. Sunrise is not going to happen for the whole world at the same time (the world is not flat😁). Hence different time zones have different offsets. If we are talking about offsets we need an offset zero time zone that aligns with the UTC which is the standard time. UTC is independent of all time zones, it is a standard. But Java treats this as same as GMT which is the timezone for London and UK. Let’s take an example. Considering that GMT (also aligns with UTC) time is 06 am now, my time zone offset(For Sri Lanka) is +5.30 which means it’s 11.30 am in my time. For the same time zone, the offset might change due to daylight savings for different periods of the year. These areas are not uniformly distributed and are highly affected by political decisions and the ease of communication hence this time might not match the desire for human working hours sometimes. For example, it is common to see a single time zone for a large country which might lead the standard time of some places to deviate from the natural time. But this is not the case always and some countries span across many time zones. Daylight Saving — To maximize the amount of natural daylight available, Daylight Saving Time (DST) is the practice of moving the clocks forward one hour from standard time in the summer and backward one hour in the autumn. I will explain this in detail with examples later. Oh!!! I almost forgot, that all the explanations above are based on the Gregorian calendar which is the modern standard calendar. That is not the only calendar available, for example, there is an Islamic calendar called the Hijri calendar and many more. Lucky for us we don’t need to implement all these things from scratch. As mentioned date time API is available in any programming language or there might be many third-party libraries that provide high-level abstract API to tackle the above-mentioned complexities. Let’s understand the Java solutions for handling date and time. Before Java 8 the date time API provided had many downsides. Let’s understand briefly the old Java date time API before Java 8 to understand why the new date time API was introduced in Java 8. Java date time API before Java 8 This API was packaged in java.util package. First, it has a Date class. Although its name is date what it represents is a point in time with millisecond precision. But in real-world scenarios, it is required to deal with dates and times separately and also needs to consider the time zones. For all these cases the pre-Java 8 solution was to use the Calendar class. Since it is a very generic class to handle all these scenarios it might easily lead to
To get the true benefit of object-oriented programming we have to define our class API cleanly by hiding the internals. Still, classes have many methods that perform specific tasks, but what if one function invocation assumes that a different function invocation already happens? This is what we call temporal coupling. This coupling of methods is not very clearly visible and hence needs more care when we implement a flow using object method invocation which needs to make it impossible to break the order of invocation. If we can eliminate temporal coupling, it’s good but reality is not that simple and we have to deal with it. Let’s consider a simple example to understand temporal coupling. Assume there is a class named File that allows us to open a file and then read or modify it and then close it. We can easily implement the methods like open, read, write, and close easily. But now the the client that uses the File class is responsible for making sure that they open the method first and then perform an action and then close it. The fact that you are not allowed to call the close method without calling the open method is a simple example of temporal coupling. Don’t confuse this example as a resource opening which can be handled with a try with resources pattern in Java. Think of this as a generic problem that can span across different classes, for example, we need to make sure that operation x in object a can be performed only if operation y in object b is performed. It would be nice if we could force the client to use the objects and invoke methods in the desired order. We can achieve this with the help of lambda functions and fluent API which can not be invoked in out of order. Let’s take the simple example of the File class. Before seeing the code of the FIle class let’s see how a client will use the File class to operate. package org.example;public class Main { public static void main(String[] args) { File.open(“file1”) .manipulateFile(Main::process) .executeAndClose(); } private static void process(File file) { System.out.println(“processing ” + file.getPath()); }} Now the API is designed in a way that the order is preserved and also this is a very readable code. Here you can assume that the manipulateFile method gets a Consumer as its argument which passes the File object to a method where we can do the reading and writing operations. Now let’s see the File class, package org.example;import java.util.function.Consumer;public class File { private final String path; private File() { this.path = null; } private File(String path) { this.path = path; this.open(); } private void open() { System.out.println(“open file ” + path); } public void close() { System.out.println(“close file ” + path); } public String getPath() { return path; } public static X open(String filePath) { return fileConsumer -> () -> { File file = new File(filePath); fileConsumer.accept(file); file.close(); }; } public interface X { Y manipulateFile(Consumer<File> fileConsumer); } public interface Y { void executeAndClose(); } // More methods to work with file content} Here I have made this File class constructor private, the only way to deal with a File is by using the static open method. Now to create a fluent API we can use used internal interface, in this case, X and Y. Why these weird names, there are not made to be reused in client application code they are the gateway to create this fluent API chain. But without making them public we can’t call the methods of these types. This is the reason I used names like X and Y which are internal implementation details but we can’t hide completely. We don’t even need to import these interfaces in our client code. One question you might ask is can’t we eliminate these interfaces and return the File object itself? Then again you haven’t solved anything File objects have more than one public method that can call in out of order. Since we creating order, we can limit our interfaces to have only one method which makes them functional interfaces. Now we can use lambda expressions to represent the implementations which is what has been done in the method open. X interface accepts a consumer and returns Y, which is also can be implemented using the lambda function. Another benefit of using the lambda function is we can delay the execution (lazy execution). In the above example, nothing will get executed until you call the executeAndClose method. This code might bit confusing if you are new to these functional programming concepts, but once you get this this will become simple more readable, and understandable for you. Hence spend some time if you didn’t get this at first sight. You might get way better ideas in the process to improve this further. Although I highlight that this technique to handle temporal coupling you can use this technique to build clean class APIs. I know that the example is a very simple one, but if you get the point of what I am trying to express you can easily convert your complex class APIs to be more cleaner by applying these techniques. Don’t be limited to this solution and take it as a boilerplate, explore on your own and come up with more creative solutions, and share it with other developers. Happy coding!!!!
How many times did you check the output of what you are testing and then add that value as the expected value in your assertions? How many times did you write many assertions in your test code that make you feel bad, but you have no option? Next time when you are in such a situation Approval testing might make your life easier. Table of Content · What is Approval Testing? · Why Use Approval Tests · The Difference Between Traditional Asserts And Approval Tests · More About Approved and Received Files · Example Use of ApprovalTests Library · Other Important Concepts And Features of The Library ∘ Reporters ∘ Scrubbers ∘ Configuration What is Approval Testing? Approval testing embraces human verification instead of simple assertions in a context where verification is not trivial. Think about a piece of code that you are trying to use to produce an image or audio, how can you do the assertion in your test? probably you might need to write additional code for the assertion, so do you need to test that code also?😁. Although computers can’t verify it as easily as humans. We can verify it very easily, by just listening to the audio or seeing the image. Approval tests embrace this factor. In approval tests, we run the tests and get the output in a verifiable way like an image an audio a text file, or any kind of file that can be verified the correctness just by looking at it. We saved this verified output as an approved file. If future tests produce anything different than the one we approved the test will fail and the important part is now you can see the difference between the previously approved file and the currently generated file using an applicable diffing tool. Now you have the idea so you might like to implement it, luckily there is a library called ApprovatTests which has its implementation in many popular programming languages. Before seeing an example let’s understand more about approval testing. Why Use Approval Tests Assert on complex test output with fewer lines of code Utilizing human intelligence for verification in an automated way Start testing legacy code more easily Facilitate customer/stakeholder collaboration in testing because they can engage in the approval process The Difference Between Traditional Asserts And Approval Tests More About Approved and Received Files When we run our test the first time it should fail because we haven’t approved anything yet. The output of the test will be created with file name suffixes with .received.* (* means file extension). To make it pass the test we have to check the content and rename this file to *.approved.* From next time onwards when a change occurs we can compare this received file with the approved file using an applicable diffing tool. So it’s clear that we should not commit the received files and we should commit the approved files. (The exact file names will be discussed in the example section). You don’t need to delete received files manually, when the received file content matches with the approved file it will be deleted by the test. So a test will fail when There is no approved file The content of approved and received files is different When an exception is raised during the test Example Use of ApprovalTests Library I will use the Java library to show the power of this technique. All the examples will be available in this GitHub repository. I will not discuss how to set up a Java project and run tests on it. If you are not familiar with Java don’t worry, the concepts will be the same and you will most probably find a library implementation for your preferred language on the ApprovatTests site. Let’s consider a traditional unit test that validates a model object using junit5. @Test public void traditionTesting() { Customer customer = customerService.createCustomer(); assertEquals(1L, customer.getId()); assertEquals(30, customer.getAge()); assertEquals(“vinod”, customer.getFirstName()); assertEquals(“madubashana”, customer.getLastName()); assertEquals(“test@gmail.com”, customer.getEmail()); assertEquals(“Sri Lanka”, customer.getCountry()); assertEquals(“address”, customer.getAddress()); assertEquals(“vinod”, customer.getUserName()); assertTrue(customer.isActive()); } Let’s use the approval tests library. (Assume there is a toString method implemented in the Customer class which is responsible for producing the printable format of objects in Java) @Test public void approvalTestingUsingTextRepresentation() { Customer customer = customerService.createCustomer(); Approvals.verify(customer); } Run this test (use your IDE, maven, gradle, or any method). At the first run, it will fail because there is nothing that is approved yet, but it creates a file named ..received.txt. The content of this file will be the string representation of the customer class (The output of the toString method in this case). If your operating system or IDE has any text diffing tool it will open automatically and show the diff this file with another file named ..approved.txt. Check the content of the received file and move it to the approved file if the output is as expected. Now you have approved the output as it is correct. As mentioned when you run the test again it will pass. The received file also will be deleted. If in the future this test fails you can compare the difference between the two files visualize what goes wrong and update the test easily. To summarize you pass verifiable content into the library and the library will produce a visually verifiable file (received file) and you verify the content and save the verified result (approved file). When a test runs and produces different content than the approved result it fails and you can easily see the difference between the approved and the current version. (Using a diffing tool, if available for the content type) Tip: There are many verify methods available for different scenarios. You can use your IDE’s code suggestions to select the correct verify method You can find more examples in this GitHub repository. Note: Sometimes your IDE might format your files automatically for example remove trailing whitespaces in lines, which can cause tests to fail each time if the received file contains trailing whitespaces. So be
