I cannot think of a better approach than your own method therefore I shall recast it in a generalized fashion.
With[{T = Thread}, normRls[l_, pat_] := l /. T[T[pat -> _] -> T[pat -> Sort[pat /. l]]] ] normRls[#, {a, b, c}] & /@ testdata // Timing // First
1.31
This is however a bit slower than your hard-coded method:
normalize /@ testdata // Timing // First
1.045
Okay, let's try a little meta-programming:
genRule[x_, {y_}] := (x -> _) :> (x -> Slot[y]) genNorm[pat_List] := With[{body = MapIndexed[genRule, pat]}, # /. (body &) @@ Sort[pat /. #] & ] genNorm[{a, b, c}] /@ testdata // Timing // First
0.889
Ah, that's more like it!
Explanation
A request for explanation of the code was made. The first method is fairly straightforward after understanding the behavior of Thread:
Thread[{a, b, c} -> {1, 2, 3}]
{a -> 1, b -> 2, c -> 3}
Thread[{a, b, c} -> nonlist]
{a -> nonlist, b -> nonlist, c -> nonlist}
This is used three separate times, to generate e.g.: {a -> _, b -> _, c -> _}
then {a -> 1, b -> 2, c -> 3}, and then to combine them into:
{(a -> _) -> a -> 1, (b -> _) -> b -> 2, (c -> _) -> c -> 3}.
The "meta-programming" method is a bit more involved.
First let's look at the result, then how we get there:
genNorm[{q, r, s}]
#1 /. ({(q -> _) :> q -> #1, (r -> _) :> r -> #2, (s -> _) :> s -> #3} &) @@ Sort[{q, r, s} /. #1] &
We see that the output is a Function (&). This function takes a single argument, the (sub)list of rules to be modified. Upon it a replacement will eventually be done (#1 /. ...). The rules for that replacement are constructed by an internal Function:
({(q -> _) :> q -> #1, (r -> _) :> r -> #2, (s -> _) :> s -> #3} &)
the parameters (#1, #2, #3) of which are filled by applying (@@) to Sort[{q, r, s} /. #1] wherein #1 is the original (sub)list of rules. Sort[{q, r, s} /. #1] itself is hopefully self-explanatory. This internal function pulls the needed parts from the sorted list. For example, with the input:
{q -> 21, r -> 11, s -> 31, t -> 21}
The output is:
{(q -> _) :> q -> 11, (r -> _) :> r -> 21, (s -> _) :> s -> 31}
Which when applied yields:
{q -> 11, r -> 21, s -> 31, t -> 21}
Okay, so how is that function constructed?
The auxiliary function genRule is MapIndexed over the pattern list:
MapIndexed[genRule, {a, b, c}]
{(a -> _) :> a -> #1, (b -> _) :> b -> #2, (c -> _) :> c -> #3}
This expression is then named body (using With) and injected into: # /. (body &) @ Sort[pat /. #] & which you should recognize as the (nested) function detailed above.