How to Test Transactions from Visa and Mastercard in a Fintech App
You can check this piece out on Hackernoon.
Hello, everyone! My name is Maxim Bogun. I am a QA Automation Engineer at Wirex, a UK-based fintech company. We offer our clients the ability to use a debit card, linking it with cryptocurrency or traditional money, and paying for goods and services. So, the main part of my job is to test the functionality related to the card transactions.
For example, a user from France can add a Bitcoin account to a Wirex card, travel to Japan and pay with the card in a local subway, and the account will be debited with the equivalent of Yens in Bitcoins. So, let me tell you briefly what these transactions are, how they are performed, and how to automate the testing of financial transaction processing.
Before we start, I'd like to highlight a bit of the terminology:
- Fiat currencies are traditional monies that exist in the real world and are generally accepted as a source of exchange, such as the dollar, euro, yen, etc. Such currencies have a central issuer, such as the state central bank.
- Cryptocurrencies are virtual currencies that, unlike traditional money, often have no central issuer and are based on a distributed ledger (blockchain) like Bitcoin, Ethereum, or a centralized ledger, for example, stablecoins like Tether.
- Visa and Mastercard are international payment systems that control 3/4 of all plastic cards in the world.
- A POS terminal is a device with an NFC chip at a store's checkout, which processes cashless payments made via a customer's card or a gadget.
- Acquirer (recipient) is a bank or other financial institution that makes mutual payments between sellers who accept cashless cards via POS terminals. The acquirer is the final recipient of the transaction.
A few thousand years ago, financial transactions were much more straightforward - ancient people grew grain, someone went hunting. Then they conditionally exchanged meat for bread, and such a barter system of mutual settlements worked for a while.
But what if the hunter no longer needed so much grain? Here, the fiat money comes into the spotlight. Fiat money is a universal means of exchange, which, thanks to the social contract, made it possible to disconnect buying and selling processes in time. In other words, with the invention of fiat money, people had the opportunity to sell their grain, put the money in their pockets, and spend it when needed instead of immediately and pointlessly exchanging grain for other goods. The production of fiat money, which is known as a national currency, is made by federal governments.
Before the invention of plastic cards, the process of mutual payments was also quite simple. But the advent of payment systems, such as Visa and Mastercard, brought more calculations than between two parties on the technical side in the transaction processing. Even though everything is effortless for the user, all they have to do is use their card to interact with the terminal and their money is immediately taken.
Let's imagine an abstract fintech company that is the final recipient (acquirer) of a transaction. Let's see what happens when a user attaches a card to a POS terminal in a store. By the way, the same scheme is valid for transfers in any electronic payment system.
As you can see, the transaction has a long way to go from the terminal to your servers and back. You may ask why there is an intermediary as a processing centre (Processor) and why you can't process messages from payment systems directly?
The answer is a matter of practicality. The processor needs to get lots of licenses and certificates from Visa and Mastercard, have huge power capacities and backup data centres, and have a large staff of specially trained people to keep track of it all. It is much easier to put this "trouble" on someone else's shoulders and just be an acquirer.
The acquirer must also be licensed by Visa and Mastercard and meet specific requirements, but all the complicated transaction processing work can be delegated to the processor.
What does a money transfer look like in the real world?
Any financial transaction must comply with the ISO 8583 standard. All terminals, ATMs, banks, payment systems and processing centres organize their requests and responses into this format.
In short, it is a set of about a hundred fields, which in a deserialized form may look like this:
Or like this in serialized:
In the original ISO message and all the documentation, these fields are called with numbers and letters, which look like this: conventionally, if 111.8 comes in, then DE39 should be 51. That's a fascinating story, trust me.
Stages of Test Automation
As you understand, only Neo from The Matrix would be able to test this manually, while an average person would take a week to analyze one of such requests and responses.
That is where automation comes to the rescue. We at our team have divided it into several parts:
- Unit tests, which developers write
- Module and integration tests. These used to be written by developers without interaction with QA, but recently we've been trying to introduce a practice of grooming QA together with devs to think through the main cases, which helps developers when writing their tests
- End-to-end tests, in which QA write in a framework specifically designed for automation (which they develop themselves). We plan to cover major scenarios, but it's just physically impossible to cover everything. Each company can modify the transaction message in its own way, so there can be so many combinations of values of fields, some of which are optional. Also, companies often use the values at their own discretion, essentially changing the message as they want.
How Transactions are Tested
According to the scheme above, any transaction from Visa or Mastercard comes to us at the gateway from our processor. But in order to test, we want to send ourselves these incoming messages. So we need a simulator that can simulate any card transactions, "routing" them to our gateway as if they came from the processor that received them from the user.
This allows us to write autotests for any scenarios. For example, if a user made a purchase in a store, then requested a refund, which was split into six parts, and then the store requested a refund on that refund. To test such a scenario we simply send ourselves this message chain and check if everything was handled correctly on our end.
This process includes:
- Checking values from hundreds of fields (the ones where DE-39 should be 51)
- Checking the user's cash balances in our application at all stages, to a precision of 10 decimal places
- Verification of records in the user's transaction history
- Verification of all currencies involved in the conversion, if any
- Checking of all rates, which were used for conversion, accurate to one billionth of a penny
- Verification of the time taken by our system to process the transaction.
Let’s look further at the transaction time. Each company must obtain their licenses to work with Visa and Mastercard, and Wirex is no exception. The speed and stability of money transfers is key to the reputation of payment systems, so there are requirements which company must meet.
Visa and Mastercard constantly monitor all transactions. Firstly, any answer from our side to the payment system's request must come in a maximum of 2 seconds. Secondly, there must be a small percentage of rejected transactions because of our decision. That's why it is crucial to automate as many scenarios as possible and be sure that the system will respond to processing any of them quickly and correctly. Visa and Mastercard have strict requirements; if at any point the company does not meet them, the license is revoked.
Automating fiat remittances testing
As for automation, the entire QA backend is involved in developing the framework and writing autotests in our company. For example, I do it five days a week. We write them on .Net Core + NUnit. And on Fridays inside the Backend QA team, we do short sessions on sharing knowledge in automation, such as C# or new features of the product itself, which aren’t well known to those who have not participated in their testing.
We do not create test cases for those tests related to transaction processing since it’s simply impossible to verify them "manually". Before each new task, tech-analysts or product owners update the specification in Jira, groom it, and then prepare tasks with a detailed description for developers.
When the functionality is ready, it’s tested in a separate environment, and then the automation task is "raised" for QA, who writes the necessary autotests. After checking the pull-request with tests, new empty test cases are created in TestRail with names that match the names of tests in the code and status is set to Automated.
We also perform obligatory regression testing to ensure that the new functionality does not affect the old logic. If any autotests suddenly stop working, the release is blocked until the reason is found. Next, QA gives approval, then the Head of QA approves, and only then does the release of new functionality happen.
Features of creating autotests
Autotests covering transaction logic for Visa and Mastercard are very similar. Sometimes it happens that the code is 95% identical. In the past, people just copied the code of a test, creating two classes and two separate tests with almost identical codes. As you understand, the amount of code grows considerably in that case, and when the time comes to change something, we have to fix two tests at once.
We implemented an approach where all code for the test is written in a base class, while the tests themselves were declared in two other classes - one for Visa, the other for Mastercard, which were inherited from the base class. The base class itself has virtual properties that are overridden in the descendants. As a result, tests in most cases simply call the base class method, where all the logic was already implemented.
Since Wirex is a global product, and each country has different regulatory requirements, this approach is especially useful when it is necessary to write tests for three different regions. Instead of copying code three times, we have just one method in the base class.
As we have a microservice architecture, we decided to speed up our autotests for transaction checking by stopping any interactions with our public API. This approach allowed us to speed up the tests by almost half. In our tests, we now "go" directly to the internal API of those microservices responsible for card transactions and user accounts. And the public API is tested by other QAs in separate tests.
Additionally, there's a feature that can have a negative impact on the stability of autotests. Because cryptocurrency rates, like BTC/USD, change rather quickly, it can often happen that there was one asset price at the beginning of the test, but by the end of the test, it had time to change. As a result, the test will fail with the error, saying that the customer's account balance does not match after the transactions. It turns out that the test is correct but becomes so-called "floating" (when the test result changes under the influence of an external factor).
There are several options to handle this - you can use either cache rates or assign "lifetime" to each rate, so at the end of the test, we will check that the actual rate was used correctly, but it was changed just during the execution flow.
As an option, you can implement a rate "freezing" functionality. You will likely want to implement this as a separate microservice, which will "live" only in a test environment so that there is no chance it will be released to Production, but this is a slippery slope.
So, I tried to give a brief overview of the transaction structure from Visa and Mastercard, the testing approaches we use, and some of the pitfalls we faced when writing the autotests. Thanks to everyone who has read to the end.