Symmetric Properties

Learn the concepts of symmetric properties and learn to write them with ease.

We'll cover the following

What are symmetric properties?

From time to time, we may find it difficult to discern what components depend on each other to succeed. Two bits of code may perform opposite actions, such as an encoder and a decoder. We need the encoder to test the decoder, and the decoder to test the encoder. In other cases, we may have a chain of operations that could be made reversible:

  • Editing text and undoing changes,
  • Translating text from French to English to Spanish and back to French.
  • Passing a message across multiple servers until it’s back to its original one.
  • Allowing a character in a game to walk in every direction until it makes its way back to its origin.

Whenever we have a reversible sequence of actions that we can assemble, we can write one of the most concise types of properties: symmetric properties.

The trick with symmetric properties is that we have to test all of the moving parts at once. If one action is the opposite of the other, then applying both operations should yield the initial input as its final output. We pass in some data, apply the reversible sequence of operations, and check that we get the initial data back. If it’s the same, then all the parts must fit well together.

So we can say that symmetric properties are tests that perform a reversible action and check if the input and output are equal to test all of the functions used.

Example

Let’s say we have a piece of code that does encoding and decoding. We could write the following property for it:

property "symmetric encoding/decoding" do
    forall data <- list({atom(), any()}) do
        data == decode(encoded)
    end
end

This property demonstrates that a list of key and value pairs can go around encoding and decoding without changing, showing that our encoding and decoding mechanisms are stable and lossless. If we subscribe to the test-driven development’s approach of “make the test pass simply and then refactor,” then we know that we’ll be able to defeat this test by writing an implementation like this one:

def encode(t), do: t 
def decode(t), do: t

The property will pass all the time. The problem is that while the chosen property is useful, it isn’t sufficient enough on its own to be a good test of encoding and decoding. It checks that the encoding and decoding together don’t lose any information, but we don’t have anything to check that data actually gets encoded at all. Let’s look at an additional property of encoding that can be added to the same test.

Get hands-on with 1200+ tech skills courses.