Building a tested calculator in the Uniface 10 IDE
Uniface is one of those tools you rarely read about, but that still runs a surprising amount of business software. I work with it regularly, and I wanted a small, self-contained project to try out a few things properly: a clean structure, input validation and - the part you almost never see in Unifa

Uniface is one of those tools you rarely read about, but that still runs a surprising amount of business software. I work with it regularly, and I wanted a small, self-contained project to try out a few things properly: a clean structure, input validation and - the part you almost never see in Uniface examples - automated tests. The result is a calculator with a modern flat look, about 25 functions (basic arithmetic, powers, roots, trigonometry, logarithms, rounding, memory, history) and a test service with 69 test cases. Everything here was built with Rocket Uniface 10 Community Edition (10.4), so you can follow along for free. CALC_FRM (Form) CALC_SVC (Service) CALC_TEST_SVC (Service) - fields & buttons ---> - public operations <--- - exec -> RUN_ALL - checks empty fields - all math - 69 test cases - shows errors - validation - PASS/FAIL via putmess - history - returns 0 / -1 + text The most important decision: the form does not calculate anything. It collects input, calls the service and displays the result. That is what makes the logic testable at all. A Uniface form needs at least one entity - if you try to compile a form without one, you get: error: 1017 - Compiling a component without entities is not allowed. For a calculator there is no table, so I created a modeled entity that is not in the database: Model: CALCULATOR_MDL Entity: CALCULATOR_DMY (Database Behavior: Not in Database) Fields: NUMBER1, NUMBER2, RESULT - data type Numeric You will see warnings like 1065 - Field ... might be truncated on output to DBMS and 1072 - Scaling ... changed to 8 when compiling. Since the entity is never written to a database, they are harmless. Create a Non-modal Form (CALC_FRM), drag the entity into it and add: the three modeled fields for input and result, non-modeled fields for CALCULATION (last calculation), MEMORY and HISTORY (multi-line edit box), one Command Button per function (BTN_ADD, BTN_SQRT, BTN_SIN, ...). The button caption is the field's Initial Value (+, βx, sin, ...). The flat look comes entirely from the widget properties, for example for the blue operator buttons: BACKCOLOR=#2563EB; FORECOLOR=#FFFFFF; BACKCOLORFILL=flat; BACKCOLORHOVER=#1D4ED8; BACKCOLORSELECT=#1E40AF; FONT=SANSLARGE TOOLTIPTEXT=Add: Number 1 + Number 2 Grey buttons for functions, indigo for memory, red for "C" - three or four colour groups are enough to make an old-school form look surprisingly current. CALC_SVC is a plain Service component. Every function is a public operation with the same shape: public operation DIVIDE params numeric pA : IN numeric pB : IN numeric pResult : OUT string pError : OUT endparams pError = "" if (pB = 0) pError = "Division by 0 is not possible." return -1 endif pResult = pA / pB return 0 end The convention is simple: return 0 on success, -1 on error, and put a readable message into pError. The caller never has to know which error happened - it just shows the text. Unary functions (SQRT, SIN, FACTORIAL, ...) have the same signature without pB. Shared helpers such as the sine/cosine series or the natural logarithm are private entry blocks inside the service, so both LN and LOG use the same code: public operation LOG params numeric pA : IN numeric pResult : OUT string pError : OUT endparams variables numeric vLn endvariables pError = "" if (pA <= 0) pError = "The logarithm is only defined for numbers greater than 0." return -1 endif call NATURAL_LOG(pA, vLn) pResult = vLn / 2.302585092994045684 return 0 end To keep the project self-contained, I implemented the transcendental functions myself in ProcScript: Taylor series for sine and cosine (after normalising the angle to -180..180 degrees), a series for ln after scaling the argument into 0.5..2, "halve, expand, square back" for e^x, and Newton's method for square roots. It is a nice exercise, and the tests below show whether the approximations are good enough. Each button's detail trigger follows the same pattern: trigger detail throws variables numeric vResult string vError endvariables call VALIDATE_INPUT if ($status < 0) return 0 endif if (NUMBER1.CALCULATOR_DMY = "" | NUMBER2.CALCULATOR_DMY = "") message/error "Please enter both numbers." return 0 endif activate "CALC_SVC".ADD(NUMBER1.CALCULATOR_DMY, NUMBER2.CALCULATOR_DMY, vResult, vError) if ($status < 0) message/error vError return 0 endif RESULT.CALCULATOR_DMY = vResult call LOG_CALCULATION($concat(NUMBER1.CALCULATOR_DMY, " + ", NUMBER2.CALCULATOR_DMY, " = ", RESULT.CALCULATOR_DMY)) end After activate, $status holds the return value of the operation, so the -1 from the service arrives directly in the form. LOG_CALCULATION is a component entry that rounds the result for display, writes it into CALCULATION and prepends it to HISTORY. In a Uniface multi-line field, %%^ is the line separator: HISTORY.CALCULATOR_DMY = $concat(vText, "%%^", HISTORY.CALCULATOR_DMY) This one surprised me. Type 2-1 into a numeric field and tab out - Uniface rejects it and keeps the focus in the field. Fine. But type 2-1 and click a button directly, and the trigger runs anyway. The field's raw text is still 2-1, while the numeric value used in the calculation is 2. So ln(2-1) happily returned 0.693... - the logarithm of 2. The fix is an explicit check before every calculation. The service has a VALIDATE_NUMBER operation that walks through the raw string: while (vPos <= vLen) vChar = pValue[vPos:1] if ($scan("0123456789", vChar) > 0) vDigits = vDigits + 1 elseif (vChar = ".") vDots = vDots + 1 elseif (vChar != "-" | vPos != 1) pError = $concat(pLabel, " is not a valid number. Use digits, one dot as decimal separator and an optional leading minus sign.") return -1 endif vPos = vPos + 1 endwhile The form calls it for both fields in its VALIDATE_INPUT entry. Accepted: 42, -3.5. Rejected: 2-1, 2,5, 1.5.2, -, abc. You don't need a test framework for this. A test service with an exec operation is enough, because Compile & Test on a service runs exec: public operation exec activate $instancename.RUN_ALL() return $status end Two small helper entries do the assertions. CHECK_VALUE compares with a tolerance (the series only approximate), CHECK_ERROR expects a negative status: entry CHECK_VALUE params string pName : IN numeric pExpected : IN numeric pActual : IN numeric pStatus : IN numeric pTests : INOUT numeric pFailures : INOUT endparams variables numeric vDiff endvariables pTests = pTests + 1 vDiff = pActual - pExpected if (vDiff < 0) vDiff = 0 - vDiff endif if (pStatus < 0 | vDiff > 0.000001) pFailures = pFailures + 1 putmess $concat($concat("FAIL: ", pName, " (expected ", pExpected), $concat(", got ", pActual, ")")) else putmess $concat("PASS: ", pName) endif end A test case is then three lines: activate "CALC_SVC".SIN(30, vResult, vError) vStatus = $status call CHECK_VALUE("SIN 30", 0.5, vResult, vStatus, vTests, vFailures) activate "CALC_SVC".TAN(90, vResult, vError) vStatus = $status call CHECK_ERROR("TAN 90", vStatus, vTests, vFailures) The suite covers every operation with normal values, edge cases and expected errors - for example ROUND(-2.5, 0) = -3, LOG(0.01) = -2, 10! = 3628800, division by zero, 0 ^ -1, 2 ^ 0.5 (only whole-number exponents are supported), FACTORIAL(2.5) and all the validation strings above. The putmess output ends up in the session log (log\ide_<pid>.log in your Uniface user folder): PASS: ADD 2 + 3 PASS: DIVIDE 1 / 0 (error expected) PASS: SIN 30 ... PASS: VALIDATE 2-1 (error expected) Calculator: 69 tests, 0 failures One caveat worth knowing: a CHECK_ERROR test would also pass if the service could not be activated at all. That is acceptable here because dozens of CHECK_VALUE tests would fail in the same situation - but if you write a suite that only checks errors, test the activation separately. $concat has an argument limit. A call with seven arguments failed with 1000 - Syntax error (Wrong number of arguments). Nesting two $concat calls fixes it. askmess vs. message/error. askmess without options shows a Yes/No dialog - odd for an error. message/error shows a proper error box with OK. The script editor has two sections. Each object in Write Script has a Declarations and a Script section. If you select all and paste a trigger, it can end up in Declarations. Paste into the Script section. Don't touch the repository while the IDE is running. The Community Edition keeps its repository in a local SQLite file (dbms\usys.db). Editing it with an external tool while the IDE was open made the IDE hang, and it would not start again until I restored a backup. Make every change through the IDE - or at least close it first and keep a copy. None of this is exotic - it is the same separation you would use in any other stack: UI in one place, logic in a service with a clear contract, and tests that call the service directly. What I like is how little Uniface needs for it: no framework, no plugins, just a service with an exec operation and putmess. If you are maintaining Uniface applications, the pattern scales: move logic out of form triggers into services one piece at a time, and put a test service next to each one before you change anything. How do you test your Uniface code - or do you? I would be curious to hear about other approaches in the comments. Disclosure: This article was created with the help of AI
Key Takeaways
- β’Uniface is one of those tools you rarely read about, but that still runs a surprising amount of business software
- β’This story was reported by Dev.to, covering developments in the dev space.
- β’AI advancements continue to reshape industries β read the full article on Dev.to for complete coverage.
π Continue reading the full article:
Read Full Article on Dev.to βShare this article



