LCD Menu: Unit & Integration Testing

a minimalist graphic art image of some lab equipment and a checklist, with the word testing beneath them.

Make the machine catch your mistakes instead of waiting for production errors

Unit and Integration testing of an LCD menu may seem very time-spendy and wasteful. I assure you that it’s potentially one of the most time-saving practices that can be done when prepping code for a microcontroller. The idea isn’t to test the actual LCD itself, but the data and decisions that empower it.


This post will cover:

  • A bit of soapboxing about why testing makes a lot of sense for microcontrollers, especially those written in Rust.

  • How to setup unit tests in a Rust project using cargo

    • Bonus: how to test using CLion IDE

  • How to swap your codebase in and out of ‘testing’ mode.

  • Example code for a unit test

  • How to actually run tests from cargo


A Mostly Brief Soapbox

Did soapboxes really contain soap? And why did people have them?

When programming a microcontroller, you compile some code, and move it onto the controller. There often isn’t much indication of what’s going on after that, other than behaviors of the system that the microcontroller is driving. And when the controller crashes, it goes dark and there isn’t much evidence as to why, especially to the inexperienced.

Use Testing To Reduce Uncertainty

Testing code creates reports that show precisely how each piece of the code will work. You can even create over-complicated tests that mimic the actual main run-time of the chip to test how lots of interfacing bits of code work together (this is integration or e2e testing).

Debug Mode During Tests

Depending on how the tooling in the developer environment, it’s possible to debug code line by line. So really with a test real-world scenarios can be created and seeing how the code will handle that can happen in real-time.

Time Savings

Every time a program is downloaded onto the device - the device must be put into UF2 mode, must receive the compiled program, and then manipulated into the state that is currently under development.

With a unit & integration test there is no need to do any of that as the development machine mocks all of that work, and makes it possible to see almost exactly what the controller will do with the same piece of code, minus all the fiddling around.

Regression Checks

Each time a test is crafted for a bit of code, that piece of code must behave as prescribed in that test in order for the tests to pass. This is great because when changes are made to that code, or to code that interfaces together, the possibility for the code to break and no longer work like expected arises.

Testing code allows the developer to catch these instances of regression. And if the developer is using TDD the real-time functionality of the code is revealed while writing it. This makes it a lot easier to see how the pieces will fit together in real-time.



How to setup tests for Rust Code using Cargo

a graphic art image showing a pencil, and a document with gears on it representing the concept of creating configurations

This part was the most time-consuming part of the entire effort for me. If I weren’t already so in love with the Red, Green, Refactor methodology of coding, I would’ve likely given up and just trial and errored it, manually testing all my code on the device.

The biggest issue I ran into getting testable units set up in this project was that the compiler was spazzing out whenever I would attempt to run tests on files that include #![no_std] in the header.

That little #![no_std] snippet is a requirement for how my PicoPonics project is set up. But it was breaking the project in any configuration I could find online, and was minutes away from breaking my sanity.

Create an External Library (crate)

I was able to create a work-around by creating a (library) crate with my code in it that I needed to test. It’s possible to quickly switch an external crate back and forth between containing the std lib, and not containing the std lib.

 

How I got the ability to run my tests:

  • move my code into a separate sibling directory to the project

  • make that directory into a crate by creating a Cargo.toml file in that directory.

  • in the module declaration for that ‘lib’, I included the #![no_std] to satisfy the compiler for the main project

  • BUT WHEN I RUN MY TESTS I comment out the #![no_std] line. I think I can find a way to make this process less manual in future, but I at least got it working.

 

My half-assed solution described above is currently in the ‘feature-lcd-menu’ of my PicoPonics repo, but I’m sure that in the next few weeks I’ll have that merged into main.

I will come back and update this post if I find a better way to do this.

A Quick Test Example

I don’t want to complete this post without at least one quick example of how my project is currently set up.

a screenshot of my code editor that shows a file directory, including the tests directory which contains all the tests for my various rust code

All of my tests are contained in a separate directory (against Rust guidelines) because I have issues with tests when my tests include

Actually Running The Tests

The best, and easiest part!

 

In terminal, in the lib project directory type:
cargo test

That is all.

 

Tip from future me:
Download
bacon, and use bacon test. Constantly running tests hanging out is pretty sweet.

This part is a breath of fresh air because it’s so simple. Cargo is very smart, and based on what’s defined in the Cargo.toml file, it checks out your project directory and finds everything that’s configured to be a test.

I did not configure Cargo itself, I only created a tests directory in the root of the project, and setup a bunch of tests in there.

And it runs every test that is present in my project, even tests that are written in the same file as the code (this is a huge recommendation from Rust) but I don’t do it because I need these files to be in a #![no_std] and the project just wont’ compile correctly if I do it for now.

If I ever learn how to get it done ‘properly’ I’ll come back and correct this.

Conclusion

Having tests able to execute saves loads of time because you don’t need to compile and load the project onto the controller to manually test functionality. It’s possible to test all modules together in large mock test contexts to ensure functionality works like expected.

It also allowed for line-by-line debugging, which is also a huge time-saver.

I have had no luck setting up tests in the same files or project as a #![no_std] project, but I was able to create an external crate, and comment on the line of the file that declares no std when I want to run tests, and then uncomment it when I want to build the project. Cargo complains for me if I forget to uncomment it so I’m not worried about accidentally putting std lib functions on a controller.

Previous
Previous

Enums: Programming with Rust

Next
Next

LCD Menu: The Hardware Setup