Probably more easily.
You get the bulk of the design right in simulation before ever moving to hardware.
Use self-checking testbenches (or higher order verification techniques including PSL, constrained random testing or OSVVM) in the role of unit testing to verify that the block meets its basic functionality.
When it doesn't, the simulator has a waveform viewer which can show you the value of every signal at every moment in time, helpfully displaying unknowns ('X' or 'Z' for example) in red. Use this judiciously to work back from incorrect results to their cause; edit, compile, re-simulate etc until the block does what you want.
Self-checking testbenches can compare the low-level (synthesisable) design against a higher level algorithmic model : any basic algorithm you can write in C can also be written in VHDL (but may not be synthesisable!) or the testbench can read the expected results from a file. Testbenches are thus development-only. (They don't generally have access to the innards of a hardware block; only it's inputs and outputs so they are black box testers). But a testbench can wire up several black boxes to test how they interact.
So, compare the actual results from a block with the expected results (you may need to store the expected results until the actuals are ready) ... this is normally done with an assert statement :
VHDL helps a lot thanks to having a decent type system : use it!
(Or get frustrated by fighting it : your choice!)
Bad VHDL :
addr : integer;
assert addr >= 0 report "Address arithmetic overflow" severity FAILURE;
Better VHDL :
subtype address is natural range 0 to 2**16 - 1;
addr : address;
The assert is now redundant because the simulator does it for you : values outside the specified range are trapped. (Synthesis can still generate overflows; the synth tools are allowed to assume you got the design right already! Therefore they won't waste hardware on overflow checks unless you explicitly code them)
You can do many good things with the type system : this ought to be second nature to SW developers but doesn't seem to be taught any more.
A couple of examples:
variable mem : array(addr) of integer;
for i in addr loop
mem(i) := 0;
Array size and loop bounds defined by the type : no loop bound errors or buffer overruns.
assert addr'high + 1 = 65536 report "Wrong address size" severity failure;
Attributes ('high, 'low, 'range (defines an integer subtype) etc allow runtime introspection of the type (set loop bounds for an unconstrained array, etc) or as here to check assumptions about a type declared somewhere else...
type colour is (red, green, blue);
a bit like a C enum except...
type RGB is array(colour) of real;
constant gain : RGB := (red => 0.31, green => 0.58, blue => 0.11);
signal YUV,RGB_in : RGB;
for c in colour loop
YUV(c) <= RGB_in(c) * gain(c);
having defined such a type you can use it as an array index or loop over it etc. And so on.
Named association for arguments, array components, etc reduce mistakes and make the code clearer.
Packages ... put together types and their operations in safe reusable components. And so on...
My designs tend to have a "common" package defining widely used things like "address" above : everything uses it and if I change the address size, all the loops readjust to the new size, and anything with hard-coded assumptions should fall over at an assert (as above) until I fix it...